diff --git a/.eslintrc.json b/.eslintrc.json index 7258c5c536..556470697d 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -26,6 +26,7 @@ "off", { "cases": { "camelCase": true, "pascalCase": true, "kebabCase": true } } ], + "import/no-import-module-exports": "off", "unicorn/no-instanceof-array": "error", "unicorn/no-static-only-class": "error", "unicorn/consistent-destructuring": "error", diff --git a/.github/workflows/prepare-for-prod-dt-deploy.yml b/.github/workflows/prepare-for-prod-dt-deploy.yml index a5ca48e3f8..fedbac8ba1 100644 --- a/.github/workflows/prepare-for-prod-dt-deploy.yml +++ b/.github/workflows/prepare-for-prod-dt-deploy.yml @@ -118,10 +118,9 @@ jobs: yq eval -i ".user-transformer.image.repository=\"$TF_IMAGE_REPOSITORY\"" multi-tenant/multi-tenant.yaml git add multi-tenant/multi-tenant.yaml - cd ../../../../config-be-rudder-transformer - yq eval -i ".config-be-rudder-transformer.image.tag=\"$TAG_NAME\"" values.prod.yaml - yq eval -i ".config-be-rudder-transformer.image.repository=\"$TF_IMAGE_REPOSITORY\"" values.prod.yaml - git add values.prod.yaml + cd ../../../../config-be-rudder-transformer/environment/prod + yq eval -i ".config-be-rudder-transformer.image.tag=\"$TAG_NAME\"" base.yaml + git add base.yaml git commit -m "chore: upgrade shared transformers to $TAG_NAME" git push -u origin shared-transformer-$TAG_NAME diff --git a/.github/workflows/prepare-for-prod-ut-deploy.yml b/.github/workflows/prepare-for-prod-ut-deploy.yml index c2900d61da..a6f7271d9c 100644 --- a/.github/workflows/prepare-for-prod-ut-deploy.yml +++ b/.github/workflows/prepare-for-prod-ut-deploy.yml @@ -102,11 +102,10 @@ jobs: cd rudder-devops git checkout -b shared-user-transformer-$UT_TAG_NAME - cd helm-charts/config-be-rudder-transformer + cd helm-charts/config-be-rudder-transformer/environment/prod - yq eval -i ".config-be-user-transformer.image.tag=\"$UT_TAG_NAME\"" values.prod.yaml - yq eval -i ".config-be-user-transformer.image.repository=\"$TF_IMAGE_REPOSITORY\"" values.prod.yaml - git add values.prod.yaml + yq eval -i ".config-be-user-transformer.image.tag=\"$UT_TAG_NAME\"" base.yaml + git add base.yaml git commit -m "chore: upgrade shared user-transformers to $UT_TAG_NAME" git push -u origin shared-user-transformer-$UT_TAG_NAME diff --git a/.github/workflows/prepare-for-staging-deploy.yml b/.github/workflows/prepare-for-staging-deploy.yml index 3e0b3aac19..46cd731d19 100644 --- a/.github/workflows/prepare-for-staging-deploy.yml +++ b/.github/workflows/prepare-for-staging-deploy.yml @@ -115,7 +115,7 @@ jobs: cd ../../../../config-be-rudder-transformer/environment/staging yq eval -i ".config-be-rudder-transformer.image.tag=\"$TAG_NAME\"" base.yaml yq eval -i ".config-be-user-transformer.image.tag=\"$TAG_NAME\"" base.yaml - git add values.staging.yaml + git add base.yaml git commit -m "chore: upgrade staging env transformers to \"$TAG_NAME\"" git push -u origin $BRANCH_NAME diff --git a/.github/workflows/publish-new-release.yml b/.github/workflows/publish-new-release.yml index 233e99577d..15d7b20fd1 100644 --- a/.github/workflows/publish-new-release.yml +++ b/.github/workflows/publish-new-release.yml @@ -99,7 +99,7 @@ jobs: channel-id: ${{ secrets.SLACK_RELEASE_CHANNEL_ID }} payload: | { - "text": "*<${{env.RELEASES_URL}}v${{ steps.extract-version.outputs.release_version }}|v${{ steps.extract-version.outputs.release_version }}>*\nCC: <@U03KG4BK1L1> <@U02AE5GMMHV> <@U01LVJ30QEB>", + "text": "*<${{env.RELEASES_URL}}v${{ steps.extract-version.outputs.release_version }}|v${{ steps.extract-version.outputs.release_version }}>*\nCC: <@U03KG4BK1L1> <@U024YF8CR53> <@U01LVJ30QEB>", "blocks": [ { "type": "header", @@ -115,7 +115,7 @@ jobs: "type": "section", "text": { "type": "mrkdwn", - "text": "*<${{env.RELEASES_URL}}v${{ steps.extract-version.outputs.release_version }}|v${{ steps.extract-version.outputs.release_version }}>*\nCC: <@U03KG4BK1L1> <@U02AE5GMMHV> <@U01LVJ30QEB>" + "text": "*<${{env.RELEASES_URL}}v${{ steps.extract-version.outputs.release_version }}|v${{ steps.extract-version.outputs.release_version }}>*\nCC: <@U03KG4BK1L1> <@U024YF8CR53> <@U01LVJ30QEB>" } } ] diff --git a/.nvmrc b/.nvmrc index 3c5535cf60..99c98cdd6a 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18.19.1 +18.20.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d3a37e122..87ca4738ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,64 @@ 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.62.2](https://github.com/rudderlabs/rudder-transformer/compare/v1.62.1...v1.62.2) (2024-04-18) + + +### Bug Fixes + +* twitter_ads logger ([#3295](https://github.com/rudderlabs/rudder-transformer/issues/3295)) ([e92b052](https://github.com/rudderlabs/rudder-transformer/commit/e92b052e03182deb41b20b3ec3741306afa50380)) + +### [1.62.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.62.0...v1.62.1) (2024-04-18) + + +### Bug Fixes + +* revert mixpanel deprecate /track ([#3291](https://github.com/rudderlabs/rudder-transformer/issues/3291)) ([ec068b4](https://github.com/rudderlabs/rudder-transformer/commit/ec068b49bd4a5652a762c60a8257c883e4709d1a)) + +## [1.62.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.61.1...v1.62.0) (2024-04-15) + + +### Features + +* do away myaxios ([#3222](https://github.com/rudderlabs/rudder-transformer/issues/3222)) ([9214594](https://github.com/rudderlabs/rudder-transformer/commit/9214594bab2c86a4ae6f75e12531f778490cf127)) +* for reddit adding currency and value for addToCart, viewConent event as well ([#3239](https://github.com/rudderlabs/rudder-transformer/issues/3239)) ([ad235e7](https://github.com/rudderlabs/rudder-transformer/commit/ad235e785bf6039e11231a915be098130b25ec3b)) +* logger upgrade in services, dest, source ([#3228](https://github.com/rudderlabs/rudder-transformer/issues/3228)) ([c204113](https://github.com/rudderlabs/rudder-transformer/commit/c204113eab37a782f217488d0d626a8d6df345d3)) +* rakuten: adding a default value for tr ([#3240](https://github.com/rudderlabs/rudder-transformer/issues/3240)) ([3748f24](https://github.com/rudderlabs/rudder-transformer/commit/3748f24e21634fc74c5e5b3761551c64c8e69942)) + + +### Bug Fixes + +* adding check for reserved key words in extract custom fields ([#3264](https://github.com/rudderlabs/rudder-transformer/issues/3264)) ([3399c47](https://github.com/rudderlabs/rudder-transformer/commit/3399c47fdce1b3d19e29306ca3c5692a2fbc30fb)) +* deployment file paths ([#3216](https://github.com/rudderlabs/rudder-transformer/issues/3216)) ([808727d](https://github.com/rudderlabs/rudder-transformer/commit/808727de17e400ed102a843ab3b30f81f8900f24)) +* email mappings ([#3247](https://github.com/rudderlabs/rudder-transformer/issues/3247)) ([791cbf5](https://github.com/rudderlabs/rudder-transformer/commit/791cbf55fc6940af4e3208212b82c891c6618fc3)) +* fixed userId mapping, now mapping to uid instead of id ([#3262](https://github.com/rudderlabs/rudder-transformer/issues/3262)) ([9c6b251](https://github.com/rudderlabs/rudder-transformer/commit/9c6b251a6c784cc391f27e846a008fbe2901e2c8)) +* hs bugsnag error ([#3252](https://github.com/rudderlabs/rudder-transformer/issues/3252)) ([9daf1c9](https://github.com/rudderlabs/rudder-transformer/commit/9daf1c989258bd410d5780c1b11c4f6df9654af5)) +* hubspot: search for contact using secondary prop ([#3258](https://github.com/rudderlabs/rudder-transformer/issues/3258)) ([0b57204](https://github.com/rudderlabs/rudder-transformer/commit/0b5720446693efe1fd0ccdfc141bd7f21b2c32ae)) +* impact: support custom product mapping ([#3249](https://github.com/rudderlabs/rudder-transformer/issues/3249)) ([cb8ff2f](https://github.com/rudderlabs/rudder-transformer/commit/cb8ff2fb943c49df4ac083bd179d9674b40eb602)) +* marketo bulk ignore null while checking data type mismatch ([#3263](https://github.com/rudderlabs/rudder-transformer/issues/3263)) ([6e3274b](https://github.com/rudderlabs/rudder-transformer/commit/6e3274bfba9e7838d1f81d845a070427b67e75f5)) +* shopify: send 500 for identifier call in case of failure ([#3235](https://github.com/rudderlabs/rudder-transformer/issues/3235)) ([8eb4c4e](https://github.com/rudderlabs/rudder-transformer/commit/8eb4c4e9b8daebbaeb1d12ff0c17915fe19c2b50)) + +### [1.61.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.61.0...v1.61.1) (2024-04-03) + +## [1.61.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.60.0...v1.61.0) (2024-04-02) + + +### Features + +* consent field support for ga4 ([#3213](https://github.com/rudderlabs/rudder-transformer/issues/3213)) ([92515a5](https://github.com/rudderlabs/rudder-transformer/commit/92515a5fd8a2798c48010078f62b360ec6a49979)) +* consent field support for gaoc and upgrade the api version from v14 to v16 ([#3121](https://github.com/rudderlabs/rudder-transformer/issues/3121)) ([2aac2a6](https://github.com/rudderlabs/rudder-transformer/commit/2aac2a62547b7a7c617735fc3d6e88e0a1bed76e)), closes [#3190](https://github.com/rudderlabs/rudder-transformer/issues/3190) +* onboard new destination bloomreach ([#3185](https://github.com/rudderlabs/rudder-transformer/issues/3185)) ([d9b7e1f](https://github.com/rudderlabs/rudder-transformer/commit/d9b7e1f70565d59979aee3e62f60e39edb9a23c7)) +* onboarding linkedin conversion api ([#3194](https://github.com/rudderlabs/rudder-transformer/issues/3194)) ([eb7b197](https://github.com/rudderlabs/rudder-transformer/commit/eb7b197322c617b14c2579de8cb4d4dacf8e1df3)) +* update movable ink batch size ([#3223](https://github.com/rudderlabs/rudder-transformer/issues/3223)) ([667095f](https://github.com/rudderlabs/rudder-transformer/commit/667095fa8316cd95a066f15b848ad503c6b4af80)) + + +### Bug Fixes + +* fixed userId mapping, now mapping to uid instead of id ([#3192](https://github.com/rudderlabs/rudder-transformer/issues/3192)) ([70a468b](https://github.com/rudderlabs/rudder-transformer/commit/70a468bf16ecd5ee0b6fecee4b837895d19c525f)) +* ninetailed: remove page support ([#3218](https://github.com/rudderlabs/rudder-transformer/issues/3218)) ([2f30c56](https://github.com/rudderlabs/rudder-transformer/commit/2f30c56af62e983d09b5d4f2da9a0ba22f5c1612)) +* shopify invalid_event metric prometheus label ([#3200](https://github.com/rudderlabs/rudder-transformer/issues/3200)) ([345c87d](https://github.com/rudderlabs/rudder-transformer/commit/345c87d7c530c621ae3fd6c504d64e5a14e31f22)) +* fix: snapchat conversion: add event level_complete ([#3231](https://github.com/rudderlabs/rudder-transformer/issues/3231)) ([39368a0](https://github.com/rudderlabs/rudder-transformer/commit/39368a09e48acc324faa855186bc623e5c347881)) + ## [1.60.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.57.1...v1.60.0) (2024-03-20) diff --git a/Dockerfile b/Dockerfile index 8cd4005a7b..9fe3c1cdb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.4 -FROM node:18.19.1-alpine3.18 AS base +FROM node:18.20.1-alpine3.18 AS base ENV HUSKY 0 RUN apk update diff --git a/package-lock.json b/package-lock.json index 5701e64a62..b5b413f936 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.60.0", + "version": "1.62.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.60.0", + "version": "1.62.2", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", @@ -18,9 +18,9 @@ "@datadog/pprof": "^3.1.0", "@koa/router": "^12.0.0", "@ndhoule/extend": "^2.0.0", - "@pyroscope/nodejs": "^0.2.6", - "@rudderstack/integrations-lib": "^0.2.4", - "@rudderstack/workflow-engine": "^0.7.2", + "@pyroscope/nodejs": "^0.2.9", + "@rudderstack/integrations-lib": "^0.2.8", + "@rudderstack/workflow-engine": "^0.7.5", "@shopify/jest-koa-mocks": "^5.1.1", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", @@ -118,41 +118,6 @@ "typescript": "^5.0.4" } }, - "../rudder-integrations-lib": { - "name": "@rudderstack/integrations-lib", - "version": "0.1.10", - "extraneous": true, - "license": "MIT", - "dependencies": { - "@rudderstack/workflow-engine": "^0.5.7", - "axios": "^1.4.0", - "axios-mock-adapter": "^1.22.0", - "crypto": "^1.0.1", - "get-value": "^3.0.1", - "handlebars": "^4.7.8", - "lodash": "^4.17.21", - "moment": "^2.29.4", - "moment-timezone": "^0.5.43", - "set-value": "^4.1.0", - "sha256": "^0.2.0", - "tslib": "^2.4.0", - "winston": "^3.11.0" - }, - "devDependencies": { - "@types/get-value": "^3.0.3", - "@types/jest": "^29.5.4", - "@types/lodash": "^4.14.195", - "@types/node": "^20.3.3", - "@types/set-value": "^4.0.1", - "@types/sha256": "^0.2.0", - "jest": "^29.4.3", - "pre-commit": "^1.2.2", - "prettier": "^2.8.4", - "ts-jest": "^29.0.5", - "ts-node": "^10.9.1", - "typescript": "^5.1.6" - } - }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -4446,33 +4411,35 @@ "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, "node_modules/@pyroscope/nodejs": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/@pyroscope/nodejs/-/nodejs-0.2.6.tgz", - "integrity": "sha512-F37ROH//HzO7zKm2S7CtNG8OAp+i4ADg4erQR9D57BrSgi8+3Jjp5s5PWqyJABC6IzsABgGrentPobBDr8QdsA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pyroscope/nodejs/-/nodejs-0.2.9.tgz", + "integrity": "sha512-pIw4pIqcNZTZxTUuV0OUI18UZEmx9lT2GaT75ny6FKVe2L1gxAwTCf5TKk8VsnUGY66buUkyaTHcTm7fy0BP/Q==", "dependencies": { - "axios": "^0.26.1", + "axios": "^0.28.0", "debug": "^4.3.3", "form-data": "^4.0.0", - "pprof": "^3.2.0", + "pprof": "^4.0.0", "regenerator-runtime": "^0.13.11", "source-map": "^0.7.3" }, "engines": { - "node": "^12.20.0 || >=14.13.1" + "node": ">=v18" } }, "node_modules/@pyroscope/nodejs/node_modules/axios": { - "version": "0.26.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", - "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz", + "integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==", "dependencies": { - "follow-redirects": "^1.14.8" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/@rudderstack/integrations-lib": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@rudderstack/integrations-lib/-/integrations-lib-0.2.4.tgz", - "integrity": "sha512-32Zose9aOPNWd4EyUNuS5YY+Vq4LYMuDcabJ+s3t1ZfHHMfISlDNF02b60MWgOrU8PARYC+siDs5wgA6xfZpzQ==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@rudderstack/integrations-lib/-/integrations-lib-0.2.8.tgz", + "integrity": "sha512-5CJoFFCRDhG7busCGVktKqEEXO0DbFqJ56TOT+jyDdoTf8sZ7SsSJ4NCZYmSplZrbQGj2R+aArnQnpxA4hPGmA==", "dependencies": { "axios": "^1.4.0", "axios-mock-adapter": "^1.22.0", @@ -4496,13 +4463,13 @@ "integrity": "sha512-+iH40g+ZA2ANgwjOITdEdZJLZV+ljR28Akn/dRoDia591tMu7PptyvDaAvl+m1DijWXddpLQ8SX9xaEcIdmqlw==" }, "node_modules/@rudderstack/workflow-engine": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@rudderstack/workflow-engine/-/workflow-engine-0.7.2.tgz", - "integrity": "sha512-aXQvoXMekvXxxDG6Yc5P5l3PJIwqVA+EmJ2w4SnQ94BUHhbsybPjgGvyzD17MUTAdWEOtqS38SuzLflBs/5T4g==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@rudderstack/workflow-engine/-/workflow-engine-0.7.5.tgz", + "integrity": "sha512-HmhxiF/gZorrEEmVvQYopIN6xicQ7kr0mHtw2fPqXmHIFLr9MnEyefo4+MPw/Re9iNFbXNQC9uKkYd7lLHbAyw==", "dependencies": { "@aws-crypto/sha256-js": "^5.0.0", "@rudderstack/json-template-engine": "^0.8.4", - "jsonata": "^2.0.3", + "jsonata": "^2.0.4", "lodash": "^4.17.21", "object-sizeof": "^2.6.3", "yaml": "^2.3.2" @@ -8907,9 +8874,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "engines": { "node": ">=8" } @@ -10498,9 +10465,9 @@ "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", @@ -14513,9 +14480,9 @@ } }, "node_modules/jsonata": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.3.tgz", - "integrity": "sha512-Up2H81MUtjqI/dWwWX7p4+bUMfMrQJVMN/jW6clFMTiYP528fBOBNtRu944QhKTs3+IsVWbgMeUTny5fw2VMUA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/jsonata/-/jsonata-2.0.4.tgz", + "integrity": "sha512-vfavX4/G/yrYxE+UrmT/oUJ3ph7KqUrb0R7b0LVRcntQwxw+Z5kA1pNUIQzX5hF04Oe1eKxyoIPsmXtc2LgJTQ==", "engines": { "node": ">= 8" } @@ -15274,6 +15241,11 @@ "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "dev": true }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==" + }, "node_modules/lodash.startcase": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", @@ -16522,9 +16494,9 @@ "dev": true }, "node_modules/nan": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", - "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" }, "node_modules/nanoid": { "version": "3.3.7", @@ -17523,24 +17495,23 @@ "dev": true }, "node_modules/pprof": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/pprof/-/pprof-3.2.1.tgz", - "integrity": "sha512-KnextTM3EHQ2zqN8fUjB0VpE+njcVR7cOfo7DjJSLKzIbKTPelDtokI04ScR/Vd8CLDj+M99tsaKV+K6FHzpzA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pprof/-/pprof-4.0.0.tgz", + "integrity": "sha512-Yhfk7Y0G1MYsy97oXxmSG5nvbM1sCz9EALiNhW/isAv5Xf7svzP+1RfGeBlS6mLSgRJvgSLh6Mi5DaisQuPttw==", "hasInstallScript": true, "dependencies": { - "@mapbox/node-pre-gyp": "^1.0.0", + "@mapbox/node-pre-gyp": "^1.0.9", "bindings": "^1.2.1", "delay": "^5.0.0", "findit2": "^2.2.3", - "nan": "^2.14.0", + "nan": "^2.17.0", "p-limit": "^3.0.0", - "pify": "^5.0.0", "protobufjs": "~7.2.4", - "source-map": "^0.7.3", + "source-map": "~0.8.0-beta.0", "split": "^1.0.1" }, "engines": { - "node": ">=10.4.1" + "node": ">=14.0.0" } }, "node_modules/pprof-format": { @@ -17548,15 +17519,38 @@ "resolved": "https://registry.npmjs.org/pprof-format/-/pprof-format-2.0.7.tgz", "integrity": "sha512-1qWaGAzwMpaXJP9opRa23nPnt2Egi7RMNoNBptEE/XwHbcn4fC2b/4U4bKc5arkGkIh2ZabpF2bEb+c5GNHEKA==" }, - "node_modules/pprof/node_modules/pify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz", - "integrity": "sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA==", - "engines": { - "node": ">=10" + "node_modules/pprof/node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dependencies": { + "whatwg-url": "^7.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 8" + } + }, + "node_modules/pprof/node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/pprof/node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "node_modules/pprof/node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" } }, "node_modules/precinct": { @@ -17966,9 +17960,9 @@ } }, "node_modules/protobufjs": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.5.tgz", - "integrity": "sha512-gGXRSXvxQ7UiPgfw8gevrfRWcTlSbOFg+p/N+JVJEK5VhueL2miT6qTymqAmjr1Q5WbOCyJbyrk6JfWKwlFn6A==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.2.6.tgz", + "integrity": "sha512-dgJaEDDL6x8ASUZ1YqWciTRrdOuYNzoOf27oHNfdyvKqHr5i0FV7FSLU+aIeFjyFgVxrpTOtQUi0BLLBymZaBw==", "hasInstallScript": true, "dependencies": { "@protobufjs/aspromise": "^1.1.2", @@ -19889,9 +19883,9 @@ } }, "node_modules/tar": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", - "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", diff --git a/package.json b/package.json index 5c1d9c1848..a291bbab90 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.60.0", + "version": "1.62.2", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { @@ -63,9 +63,9 @@ "@datadog/pprof": "^3.1.0", "@koa/router": "^12.0.0", "@ndhoule/extend": "^2.0.0", - "@pyroscope/nodejs": "^0.2.6", - "@rudderstack/integrations-lib": "^0.2.4", - "@rudderstack/workflow-engine": "^0.7.2", + "@pyroscope/nodejs": "^0.2.9", + "@rudderstack/integrations-lib": "^0.2.8", + "@rudderstack/workflow-engine": "^0.7.5", "@shopify/jest-koa-mocks": "^5.1.1", "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", diff --git a/src/cdk/v2/destinations/bloomreach/config.ts b/src/cdk/v2/destinations/bloomreach/config.ts new file mode 100644 index 0000000000..90fbcc63c6 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach/config.ts @@ -0,0 +1,30 @@ +import { getMappingConfig } from '../../../../v0/util'; + +export const CUSTOMER_COMMAND = 'customers'; +export const CUSTOMER_EVENT_COMMAND = 'customers/events'; +export const MAX_BATCH_SIZE = 50; + +// ref:- https://documentation.bloomreach.com/engagement/reference/batch-commands-2 +export const getBatchEndpoint = (apiBaseUrl: string, projectToken: string): string => + `${apiBaseUrl}/track/v2/projects/${projectToken}/batch`; + +const CONFIG_CATEGORIES = { + CUSTOMER_PROPERTIES_CONFIG: { name: 'BloomreachCustomerPropertiesConfig' }, +}; +const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); +export const EXCLUSION_FIELDS: string[] = [ + 'email', + 'firstName', + 'firstname', + 'first_name', + 'lastName', + 'lastname', + 'last_name', + 'name', + 'phone', + 'city', + 'birthday', + 'country', +]; +export const CUSTOMER_PROPERTIES_CONFIG = + MAPPING_CONFIG[CONFIG_CATEGORIES.CUSTOMER_PROPERTIES_CONFIG.name]; diff --git a/src/cdk/v2/destinations/bloomreach/data/BloomreachCustomerPropertiesConfig.json b/src/cdk/v2/destinations/bloomreach/data/BloomreachCustomerPropertiesConfig.json new file mode 100644 index 0000000000..cb4c2f7201 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach/data/BloomreachCustomerPropertiesConfig.json @@ -0,0 +1,36 @@ +[ + { + "destKey": "first_name", + "sourceKeys": "firstName", + "sourceFromGenericMap": true + }, + { + "destKey": "last_name", + "sourceKeys": "lastName", + "sourceFromGenericMap": true + }, + { + "destKey": "email", + "sourceKeys": "emailOnly", + "sourceFromGenericMap": true + }, + { + "destKey": "phone", + "sourceKeys": "phone", + "sourceFromGenericMap": true + }, + { + "destKey": "city", + "sourceKeys": "city", + "sourceFromGenericMap": true + }, + { + "destKey": "country", + "sourceKeys": ["traits.address.country", "context.traits.address.country"] + }, + { + "destKey": "birthday", + "sourceKeys": "birthday", + "sourceFromGenericMap": true + } +] diff --git a/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml b/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml new file mode 100644 index 0000000000..f092d90382 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach/procWorkflow.yaml @@ -0,0 +1,119 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + - name: defaultRequestConfig + path: ../../../../v0/util + - name: toUnixTimestamp + path: ../../../../v0/util + - name: base64Convertor + path: ../../../../v0/util + - name: removeUndefinedAndNullValues + path: ../../../../v0/util + - name: generateExclusionList + path: ../../../../v0/util + - name: extractCustomFields + path: ../../../../v0/util + - name: constructPayload + path: ../../../../v0/util + - path: ./utils + - path: ./config + +steps: + - name: messageType + template: | + $.context.messageType = .message.type.toLowerCase(); + + - name: validateInput + template: | + let messageType = $.context.messageType; + $.assert(messageType, "message Type is not present. Aborting"); + $.assert(messageType in {{$.EventType.([.IDENTIFY,.TRACK,.PAGE,.SCREEN])}}, "message type " + messageType + " is not supported"); + $.assertConfig(.destination.Config.apiBaseUrl, "API Base URL is not present. Aborting"); + $.assertConfig(.destination.Config.apiKey, "API Key is not present . Aborting"); + $.assertConfig(.destination.Config.apiSecret, "API Secret is not present. Aborting"); + $.assertConfig(.destination.Config.projectToken, "Project Token is not present. Aborting"); + $.assertConfig(.destination.Config.hardID, "Hard ID is not present. Aborting"); + $.assertConfig(.destination.Config.softID, "Soft ID is not present. Aborting"); + $.assert(.message.timestamp ?? .message.originalTimestamp, "Timestamp is not present. Aborting"); + const userId = .message.().( + {{{{$.getGenericPaths("userIdOnly")}}}}; + ); + $.assert(userId ?? .message.anonymousId, "Either one of userId or anonymousId is required. Aborting"); + + - name: prepareIdentifyPayload + condition: $.context.messageType === {{$.EventType.IDENTIFY}} + template: | + const customerIDs = $.prepareCustomerIDs(.message, .destination); + const customerProperties = $.constructPayload(.message, $.CUSTOMER_PROPERTIES_CONFIG); + const extraCustomerProperties = $.extractCustomFields(.message, {}, ['traits', 'context.traits'], $.EXCLUSION_FIELDS); + const properties = { + ...customerProperties, + ...extraCustomerProperties + } + const data = .message.().({ + "customer_ids": customerIDs, + "update_timestamp": $.toUnixTimestamp({{{{$.getGenericPaths("timestamp")}}}}), + properties + }); + + $.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_COMMAND, data}) + + - name: prepareEventName + steps: + - name: pageEventName + condition: $.context.messageType === {{$.EventType.PAGE}} + template: | + const category = .message.category ?? .message.properties.category; + const name = .message.name || .message.properties.name; + const eventNameArray = ["Viewed"]; + category ? eventNameArray.push(category); + name ? eventNameArray.push(name); + eventNameArray.push("Page"); + $.context.event = eventNameArray.join(" "); + - name: screenEventName + condition: $.context.messageType === {{$.EventType.SCREEN}} + template: | + const category = .message.category ?? .message.properties.category; + const name = .message.name || .message.properties.name; + const eventNameArray = ["Viewed"]; + category ? eventNameArray.push(category); + name ? eventNameArray.push(name); + eventNameArray.push("Screen"); + $.context.event = eventNameArray.join(" "); + - name: trackEventName + condition: $.context.messageType === {{$.EventType.TRACK}} + template: | + $.assert(.message.event, "Event name is required. Aborting"); + $.context.event = .message.event + + - name: prepareTrackPageScreenPayload + condition: $.context.messageType !== {{$.EventType.IDENTIFY}} + template: | + const customerIDs = $.prepareCustomerIDs(.message, .destination); + const data = .message.().({ + "customer_ids": customerIDs, + "timestamp": $.toUnixTimestamp({{{{$.getGenericPaths("timestamp")}}}}), + "properties": .properties, + "event_type": $.context.event, + }); + + $.context.payload = $.removeUndefinedAndNullValues({name: $.CUSTOMER_EVENT_COMMAND, data}) + + - name: buildResponse + description: In batchMode we return payload directly + condition: $.batchMode + template: | + $.context.payload + else: + name: buildResponseForProcessTransformation + template: | + const response = $.defaultRequestConfig(); + response.body.JSON = $.context.payload; + response.endpoint = $.getBatchEndpoint(.destination.Config.apiBaseUrl, .destination.Config.projectToken); + response.method = "POST"; + response.headers = { + "Content-Type": "application/json", + "Authorization": "Basic " + $.base64Convertor(.destination.Config.apiKey + ":" + .destination.Config.apiSecret) + } + response; diff --git a/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml b/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml new file mode 100644 index 0000000000..b8b27ca02e --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach/rtWorkflow.yaml @@ -0,0 +1,76 @@ +bindings: + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + - path: ./utils + exportAll: true + - name: base64Convertor + path: ../../../../v0/util + - name: toUnixTimestamp + path: ../../../../v0/util + - name: BatchUtils + path: '@rudderstack/workflow-engine' + - path: ./config + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "batchedRequest": ., + "batched": false, + "destination": ^[idx].destination, + "metadata": ^[idx].metadata, + "statusCode": 200 + })[] + + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + let batches = $.BatchUtils.chunkArrayBySizeAndLength( + $.outputs.successfulEvents, {maxItems: $.MAX_BATCH_SIZE}).items; + + batches@batch.({ + "batchedRequest": { + "body": { + "JSON": {"commands": ~r batch.batchedRequest[]}, + "JSON_ARRAY": {}, + "XML": {}, + "FORM": {} + }, + "version": "1", + "type": "REST", + "method": "POST", + "endpoint": batch[0].destination.Config.().($.getBatchEndpoint(.apiBaseUrl, .projectToken)), + "headers": batch[0].destination.Config.().({ + "Content-Type": "application/json", + "Authorization": "Basic " + $.base64Convertor(.apiKey + ":" + .apiSecret) + }), + "params": {}, + "files": {} + }, + "metadata": ~r batch.metadata[], + "batched": true, + "statusCode": 200, + "destination": batch[0].destination + })[]; + + - name: finalPayload + template: | + [...$.outputs.batchSuccessfulEvents, ...$.outputs.failedEvents] diff --git a/src/cdk/v2/destinations/bloomreach/utils.ts b/src/cdk/v2/destinations/bloomreach/utils.ts new file mode 100644 index 0000000000..f834fa74e7 --- /dev/null +++ b/src/cdk/v2/destinations/bloomreach/utils.ts @@ -0,0 +1,31 @@ +import { isObject, isEmptyObject, getIntegrationsObj } from '../../../../v0/util'; +import { RudderMessage, Destination } from '../../../../types'; + +const getCustomerIDsFromIntegrationObject = (message: RudderMessage): any => { + const integrationObj = getIntegrationsObj(message, 'bloomreach' as any) || {}; + const { hardID, softID } = integrationObj; + const customerIDs = {}; + + if (isObject(hardID) && !isEmptyObject(hardID)) { + Object.keys(hardID).forEach((id) => { + customerIDs[id] = hardID[id]; + }); + } + + if (isObject(softID) && !isEmptyObject(softID)) { + Object.keys(softID).forEach((id) => { + customerIDs[id] = softID[id]; + }); + } + + return customerIDs; +}; + +export const prepareCustomerIDs = (message: RudderMessage, destination: Destination): any => { + const customerIDs = { + [destination.Config.hardID]: message.userId, + [destination.Config.softID]: message.anonymousId, + ...getCustomerIDsFromIntegrationObject(message), + }; + return customerIDs; +}; diff --git a/src/cdk/v2/destinations/bluecore/config.js b/src/cdk/v2/destinations/bluecore/config.js index 9b9cde9c66..98e1bb4b23 100644 --- a/src/cdk/v2/destinations/bluecore/config.js +++ b/src/cdk/v2/destinations/bluecore/config.js @@ -1,6 +1,6 @@ const { getMappingConfig } = require('../../../../v0/util'); -const BASE_URL = 'https://api.bluecore.com/api/track/mobile/v1'; +const BASE_URL = 'https://api.bluecore.app/api/track/mobile/v1'; const CONFIG_CATEGORIES = { IDENTIFY: { @@ -46,6 +46,24 @@ const EVENT_NAME_MAPPING = [ const BLUECORE_EXCLUSION_FIELDS = ['query', 'order_id', 'total']; +const IDENTIFY_EXCLUSION_LIST = [ + 'name', + 'firstName', + 'first_name', + 'firstname', + 'lastName', + 'last_name', + 'lastname', + 'email', + 'age', + 'sex', + 'address', + 'action', + 'event', +]; + +const TRACK_EXCLUSION_LIST = [...IDENTIFY_EXCLUSION_LIST, 'query', 'order_id', 'total', 'products']; + const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); module.exports = { CONFIG_CATEGORIES, @@ -53,4 +71,6 @@ module.exports = { EVENT_NAME_MAPPING, BASE_URL, BLUECORE_EXCLUSION_FIELDS, + IDENTIFY_EXCLUSION_LIST, + TRACK_EXCLUSION_LIST, }; diff --git a/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json b/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json index be74c7c4b3..536f77a045 100644 --- a/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json +++ b/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json @@ -35,7 +35,7 @@ }, { "destKey": "properties.customer.email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/cdk/v2/destinations/bluecore/procWorkflow.yaml b/src/cdk/v2/destinations/bluecore/procWorkflow.yaml index 480bced699..9828ac593c 100644 --- a/src/cdk/v2/destinations/bluecore/procWorkflow.yaml +++ b/src/cdk/v2/destinations/bluecore/procWorkflow.yaml @@ -26,7 +26,7 @@ steps: condition: $.outputs.messageType === {{$.EventType.IDENTIFY}} template: | const payload = $.constructProperties(.message); - payload.token = .destination.Config.bluecoreNamespace; + payload.properties.token = .destination.Config.bluecoreNamespace; $.verifyPayload(payload, .message); payload.event = payload.event ?? 'customer_patch'; payload.properties.distinct_id = $.populateAccurateDistinctId(payload, .message); @@ -50,7 +50,7 @@ steps: const temporaryProductArray = newPayload.properties.products ?? $.createProductForStandardEcommEvent(^.message, eventName); newPayload.properties.products = $.normalizeProductArray(temporaryProductArray); newPayload.event = eventName; - newPayload.token = ^.destination.Config.bluecoreNamespace; + newPayload.properties.token = ^.destination.Config.bluecoreNamespace; $.verifyPayload(newPayload, ^.message); $.removeUndefinedNullValuesAndEmptyObjectArray(newPayload) )[]; @@ -61,7 +61,7 @@ steps: const response = $.defaultRequestConfig(); response.body.JSON = .; response.method = "POST"; - response.endpoint = "https://api.bluecore.com/api/track/mobile/v1"; + response.endpoint = "https://api.bluecore.app/api/track/mobile/v1"; response.headers = { "Content-Type": "application/json" }; diff --git a/src/cdk/v2/destinations/bluecore/utils.js b/src/cdk/v2/destinations/bluecore/utils.js index 22ec254fe2..91eda60d0d 100644 --- a/src/cdk/v2/destinations/bluecore/utils.js +++ b/src/cdk/v2/destinations/bluecore/utils.js @@ -12,9 +12,10 @@ const { validateEventName, constructPayload, getDestinationExternalID, + extractCustomFields, } = require('../../../../v0/util'); const { CommonUtils } = require('../../../../util/common'); -const { EVENT_NAME_MAPPING } = require('./config'); +const { EVENT_NAME_MAPPING, IDENTIFY_EXCLUSION_LIST, TRACK_EXCLUSION_LIST } = require('./config'); const { EventType } = require('../../../../constants'); const { MAPPING_CONFIG, CONFIG_CATEGORIES } = require('./config'); @@ -167,6 +168,41 @@ const normalizeProductArray = (products) => { return finalProductArray; }; +const mapCustomProperties = (message) => { + let customerProperties; + const customProperties = { properties: {} }; + const messageType = message.type.toUpperCase(); + switch (messageType) { + case 'IDENTIFY': + customerProperties = extractCustomFields( + message, + {}, + ['traits', 'context.traits'], + IDENTIFY_EXCLUSION_LIST, + ); + customProperties.properties.customer = customerProperties; + break; + case 'TRACK': + customerProperties = extractCustomFields( + message, + {}, + ['traits', 'context.traits'], + IDENTIFY_EXCLUSION_LIST, + ); + customProperties.properties = extractCustomFields( + message, + {}, + ['properties'], + TRACK_EXCLUSION_LIST, + ); + customProperties.properties.customer = customerProperties; + break; + default: + break; + } + return customProperties; +}; + /** * Constructs properties based on the given message. * @@ -178,7 +214,12 @@ const constructProperties = (message) => { const commonPayload = constructPayload(message, MAPPING_CONFIG[commonCategory.name]); const category = CONFIG_CATEGORIES[message.type.toUpperCase()]; const typeSpecificPayload = constructPayload(message, MAPPING_CONFIG[category.name]); - const finalPayload = lodash.merge(commonPayload, typeSpecificPayload); + const typeSpecificCustomProperties = mapCustomProperties(message); + const finalPayload = lodash.merge( + commonPayload, + typeSpecificPayload, + typeSpecificCustomProperties, + ); return finalPayload; }; diff --git a/src/cdk/v2/destinations/linkedin_ads/config.js b/src/cdk/v2/destinations/linkedin_ads/config.js new file mode 100644 index 0000000000..344980e7d0 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_ads/config.js @@ -0,0 +1,26 @@ +const { getMappingConfig } = require('../../../../v0/util'); + +// ref : https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-02&tabs=http#adding-multiple-conversion-events-in-a-batch +const BATCH_ENDPOINT = 'https://api.linkedin.com/rest/conversionEvents'; +const API_HEADER_METHOD = 'BATCH_CREATE'; +const API_VERSION = '202402'; // yyyymm format +const API_PROTOCOL_VERSION = '2.0.0'; + +const CONFIG_CATEGORIES = { + USER_INFO: { + name: 'linkedinUserInfoConfig', + type: 'user', + }, +}; + +const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); + +module.exports = { + MAX_BATCH_SIZE: 5000, + BATCH_ENDPOINT, + API_HEADER_METHOD, + API_VERSION, + API_PROTOCOL_VERSION, + CONFIG_CATEGORIES, + MAPPING_CONFIG, +}; diff --git a/src/cdk/v2/destinations/linkedin_ads/data/linkedinUserInfoConfig.json b/src/cdk/v2/destinations/linkedin_ads/data/linkedinUserInfoConfig.json new file mode 100644 index 0000000000..760510b5b3 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_ads/data/linkedinUserInfoConfig.json @@ -0,0 +1,31 @@ +[ + { + "destKey": "firstName", + "sourceKeys": "firstName", + "required": false, + "sourceFromGenericMap": true + }, + { + "destKey": "lastName", + "sourceKeys": "lastName", + "required": false, + "sourceFromGenericMap": true + }, + { + "destKey": "title", + "sourceKeys": "title", + "required": false, + "sourceFromGenericMap": true + }, + { + "destKey": "companyName", + "sourceKeys": "context.traits.companyName", + "required": false + }, + { + "destKey": "countryCode", + "sourceKeys": "countryCode", + "sourceFromGenericMap": true, + "required": false + } +] diff --git a/src/cdk/v2/destinations/linkedin_ads/procWorkflow.yaml b/src/cdk/v2/destinations/linkedin_ads/procWorkflow.yaml new file mode 100644 index 0000000000..4b17afc368 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_ads/procWorkflow.yaml @@ -0,0 +1,80 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + exportAll: true + - name: removeUndefinedValues + path: ../../../../v0/util + - name: removeUndefinedNullValuesAndEmptyObjectArray + path: ../../../../v0/util + - name: defaultRequestConfig + path: ../../../../v0/util + - name: OAuthSecretError + path: '@rudderstack/integrations-lib' + - path: ./utils + - path: ./config + - path: lodash + name: cloneDeep + +steps: + - name: checkIfProcessed + condition: .message.statusCode + template: | + $.batchMode ? .message.body.JSON : .message + onComplete: return + - name: messageType + template: | + .message.type.toLowerCase() + - name: validateInput + template: | + let messageType = $.outputs.messageType; + $.assert(messageType, "Message type is not present. Aborting message."); + $.assert(messageType in {{$.EventType.([.TRACK])}}, + "message type " + messageType + " is not supported") + + - name: validateInputForTrack + description: Additional validation for Track events + condition: $.outputs.messageType === {{$.EventType.TRACK}} + template: | + $.assert(.message.event, "event could not be mapped to conversion rule. Aborting.") + - name: commonFields + description: | + Builds common fields in destination payload. + ref: https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-02&tabs=curl#adding-multiple-conversion-events-in-a-batch + template: | + let commonFields = .message.().({ + "conversionHappenedAt": $.fetchAndVerifyConversionHappenedAt(^.message), + "eventId": $.getOneByPaths(., ^.destination.Config.deduplicationKey) ?? .messageId, + "conversionValue":$.calculateConversionObject(^.message), + "user":{ + "userIds":$.fetchUserIds(^.message,^.destination.Config), + "userInfo":$.curateUserInfoObject(^.message) + } + }); + $.removeUndefinedValues(commonFields) + - name: basePayload + template: | + const payload = $.outputs.commonFields; + payload + + - name: deduceConversionEventRules + template: | + $.context.deduceConversionRulesArray = $.deduceConversionRules(.message.event,.destination.Config) + + - name: preparePayload + template: | + $.context.payloads = $.context.deduceConversionRulesArray@conversionRuleId.( + const newPayload = $.cloneDeep($.outputs.basePayload); + newPayload.conversion = $.createConversionString(conversionRuleId); + $.removeUndefinedNullValuesAndEmptyObjectArray(newPayload) + )[]; + - name: buildResponse + template: | + $.assertThrow((.metadata.secret && .metadata.secret.accessToken), new $.OAuthSecretError("Secret or accessToken is not present in the metadata")) + const accessToken = .metadata.secret.accessToken + const response = $.defaultRequestConfig(); + response.body.JSON = {elements: $.context.payloads}; + response.endpoint = $.BATCH_ENDPOINT; + response.method = "POST"; + response.headers = $.generateHeader(accessToken) + response diff --git a/src/cdk/v2/destinations/linkedin_ads/rtWorkflow.yaml b/src/cdk/v2/destinations/linkedin_ads/rtWorkflow.yaml new file mode 100644 index 0000000000..dda322e45e --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_ads/rtWorkflow.yaml @@ -0,0 +1,39 @@ +bindings: + - path: ./utils + - path: ./config + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + bindings: + - name: batchMode + value: true + loopOverInput: true + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "message": .[], + "destination": ^ [idx].destination, + "metadata": ^ [idx].metadata + })[] + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.failedEvents, ...$.outputs.batchSuccessfulEvents] diff --git a/src/cdk/v2/destinations/linkedin_ads/utils.js b/src/cdk/v2/destinations/linkedin_ads/utils.js new file mode 100644 index 0000000000..69fea4299d --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_ads/utils.js @@ -0,0 +1,285 @@ +const lodash = require('lodash'); +const crypto = require('crypto'); +const moment = require('moment'); + +const { + InstrumentationError, + getHashFromArrayWithDuplicate, + isDefinedAndNotNullAndNotEmpty, + ConfigurationError, +} = require('@rudderstack/integrations-lib'); +const { + getFieldValueFromMessage, + constructPayload, + getDestinationExternalID, +} = require('../../../../v0/util'); +const { + MAPPING_CONFIG, + CONFIG_CATEGORIES, + MAX_BATCH_SIZE, + API_HEADER_METHOD, + API_PROTOCOL_VERSION, + API_VERSION, +} = require('./config'); +const { + AUTH_STATUS_INACTIVE, + REFRESH_TOKEN, +} = require('../../../../adapters/networkhandler/authConstants'); + +const formatEmail = (email, destConfig) => { + if (email) { + if (destConfig.hashData === true) { + return crypto.createHash('sha256').update(email).digest('hex'); + } + return email; + } + return null; +}; + +const fetchUserIds = (message, destConfig) => { + const userIds = []; + const email = formatEmail(getFieldValueFromMessage(message, 'email'), destConfig); + const linkedinFirstPartyAdsTrackingUUID = getDestinationExternalID( + message, + 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + ); + const acxiomId = getDestinationExternalID(message, 'ACXIOM_ID'); + const oracleMoatId = getDestinationExternalID(message, 'ORACLE_MOAT_ID'); + if (!email && !linkedinFirstPartyAdsTrackingUUID && !acxiomId && !oracleMoatId) { + throw new InstrumentationError( + '[LinkedIn Conversion API] no matching user id found. Please provide at least one of the following: email, linkedinFirstPartyAdsTrackingUUID, acxiomId, oracleMoatId', + ); + } + + if (email) { + userIds.push({ idType: 'SHA256_EMAIL', idValue: email }); + } + if (linkedinFirstPartyAdsTrackingUUID) { + userIds.push({ + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: linkedinFirstPartyAdsTrackingUUID, + }); + } + if (acxiomId) { + userIds.push({ idType: 'ACXIOM_ID', idValue: acxiomId }); + } + if (oracleMoatId) { + userIds.push({ idType: 'ORACLE_MOAT_ID', idValue: oracleMoatId }); + } + return userIds; +}; + +const curateUserInfoObject = (message) => { + const commonCategory = CONFIG_CATEGORIES.USER_INFO; + const commonPayload = constructPayload(message, MAPPING_CONFIG[commonCategory.name]); + if (commonPayload.firstName && commonPayload.lastName) { + return commonPayload; + } + return null; +}; + +function checkIfPricePresent(properties) { + // Check if 'products' exists and is an array + if (Array.isArray(properties?.products)) { + // Use 'some' to check if at least one object has a 'price' field + const hasPrice = properties.products.some((product) => product.hasOwnProperty('price')); + return hasPrice; + } + return !!properties.price; +} + +const calculateConversionObject = (message) => { + const { properties, event } = message; + + const calculateAmount = () => { + if (properties?.products && properties.products.length > 0) { + return properties.products.reduce( + (acc, product) => acc + (product.price || 0) * (product.quantity || 1), + 0, + ); + } + return properties.price * (properties.quantity ?? 1); + }; + if (checkIfPricePresent(properties)) { + const conversionObject = { + currencyCode: properties.currency || 'USD', + amount: `${calculateAmount()}`, + }; + return conversionObject; + } + throw new InstrumentationError( + `[LinkedIn Conversion API]: Cannot map price for event ${event}. Aborting`, + ); +}; + +const deduceConversionRules = (trackEventName, destConfig) => { + let conversionRule; + const { conversionMapping } = destConfig; + if (conversionMapping.length > 0) { + const keyMap = getHashFromArrayWithDuplicate(conversionMapping, 'from', 'to', false); + conversionRule = keyMap[trackEventName]; + } + if (isDefinedAndNotNullAndNotEmpty(conversionRule)) { + const finalEvent = typeof conversionRule === 'string' ? [conversionRule] : [...conversionRule]; + return finalEvent; + } + throw new ConfigurationError( + `[LinkedIn Conversion API] no matching conversion rule found for ${trackEventName}. Please provide a conversion rule. Aborting`, + ); +}; + +const createConversionString = (ruleId) => `urn:lla:llaPartnerConversion:${ruleId}`; + +const generateHeader = (accessToken) => { + const headers = { + 'Content-Type': 'application/json', + 'X-RestLi-Method': API_HEADER_METHOD, + 'X-Restli-Protocol-Version': API_PROTOCOL_VERSION, + 'LinkedIn-Version': API_VERSION, + Authorization: `Bearer ${accessToken}`, + }; + return headers; +}; + +const fetchAndVerifyConversionHappenedAt = (message) => { + const timeStamp = message.timestamp || message.originalTimestamp; + if (timeStamp) { + const start = moment(timeStamp); + if (!start.isValid()) { + throw new InstrumentationError('Invalid timestamp format.'); + } + const current = moment(); + // calculates past event in days + const deltaDay = current.diff(start, 'days', true); + + if (Math.ceil(deltaDay) > 90) { + throw new InstrumentationError('Events must be sent within ninety days of their occurrence.'); + } + } + + const timeInMilliseconds = moment(timeStamp).valueOf(); // `valueOf` returns the time in milliseconds + return timeInMilliseconds; +}; + +function batchResponseBuilder(successfulEvents) { + if (successfulEvents.length === 0) { + return []; + } + const constants = { + version: successfulEvents[0].message[0].version, + type: successfulEvents[0].message[0].type, + method: successfulEvents[0].message[0].method, + endpoint: successfulEvents[0].message[0].endpoint, + headers: successfulEvents[0].message[0].headers, + destination: successfulEvents[0].destination, + }; + + const allElements = successfulEvents.flatMap((event) => event.message[0].body.JSON.elements); + const allMetadata = successfulEvents.map((event) => event.metadata); + + // Using lodash to chunk the elements into groups of up to 3 + const chunkedElements = lodash.chunk(allElements, MAX_BATCH_SIZE); + const chunkedMetadata = lodash.chunk(allMetadata, MAX_BATCH_SIZE); + + return chunkedElements.map((elementsBatch, index) => ({ + batchedRequest: { + body: { + JSON: { elements: elementsBatch }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: constants.version, + type: constants.type, + method: constants.method, + endpoint: constants.endpoint, + headers: constants.headers, + params: {}, + files: {}, + }, + metadata: chunkedMetadata[index], + batched: true, + statusCode: 200, + destination: constants.destination, + })); +} + +function constructPartialStatus(errorMessage) { + const errorPattern = /Index: (\d+), ERROR :: (.*?)\n/g; + let match; + const errorMap = {}; + + try { + // eslint-disable-next-line no-cond-assign + while ((match = errorPattern.exec(errorMessage)) !== null) { + const [, index, message] = match; + errorMap[index] = message; + } + + return errorMap; + } catch (e) { + return null; + } +} + +function createResponseArray(metadata, partialStatus) { + const partialStatusArray = Object.entries(partialStatus).map(([index, message]) => [ + Number(index), + message, + ]); + // Convert destPartialStatus to an object for easier lookup + const errorMap = partialStatusArray.reduce((acc, [index, message]) => { + const jobId = metadata[index]?.jobId; // Get the jobId from the metadata array based on the index + if (jobId !== undefined) { + acc[jobId] = message; + } + return acc; + }, {}); + + return metadata.map((item) => { + const error = errorMap[item.jobId]; + return { + statusCode: error ? 400 : 500, + metadata: item, + error: error || 'success', + }; + }); +} + +/** + * + * @param {*} destinationResponse example: {status : 401, response {"status":401,"serviceErrorCode":65601,"code":"REVOKED_ACCESS_TOKEN","message":"The token used in the request has been revoked by the user"}} + * @returns proper auth error category + */ +const getAuthErrCategoryFromStCode = (destinationResponse) => { + const { status, response } = destinationResponse; + if (status === 401) { + if (response.code === 'REVOKED_ACCESS_TOKEN') { + // ACCESS_DENIED + return AUTH_STATUS_INACTIVE; + } + // UNAUTHORIZED + return REFRESH_TOKEN; + } + if (status === 403) { + // ACCESS_DENIED + return AUTH_STATUS_INACTIVE; + } + return ''; +}; + +module.exports = { + formatEmail, + calculateConversionObject, + curateUserInfoObject, + fetchUserIds, + deduceConversionRules, + createConversionString, + generateHeader, + fetchAndVerifyConversionHappenedAt, + batchResponseBuilder, + constructPartialStatus, + createResponseArray, + checkIfPricePresent, + getAuthErrCategoryFromStCode, +}; diff --git a/src/cdk/v2/destinations/linkedin_ads/utils.test.js b/src/cdk/v2/destinations/linkedin_ads/utils.test.js new file mode 100644 index 0000000000..ee52928198 --- /dev/null +++ b/src/cdk/v2/destinations/linkedin_ads/utils.test.js @@ -0,0 +1,293 @@ +const crypto = require('crypto'); +const { + formatEmail, + calculateConversionObject, + fetchUserIds, + curateUserInfoObject, + deduceConversionRules, + generateHeader, + constructPartialStatus, + createResponseArray, + checkIfPricePresent, +} = require('./utils'); +const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); +const { API_HEADER_METHOD, API_PROTOCOL_VERSION, API_VERSION } = require('./config'); + +describe('formatEmail', () => { + // Returns a hashed email when a valid email is passed as argument. + it('should return a hashed email when a valid email is passed as argument', () => { + const email = 'test@example.com'; + const hashedEmail = crypto.createHash('sha256').update(email).digest('hex'); + expect(formatEmail(email, { hashData: true })).toEqual(hashedEmail); + }); + + // Returns null when an empty string is passed as argument. + it('should return null when an empty string is passed as argument', () => { + const email = ''; + expect(formatEmail(email)).toBeNull(); + }); +}); + +describe('calculateConversionObject', () => { + // Returns a conversion object with currency code 'USD' and amount 0 when message properties are empty + it('should throw instrumentation error when message properties are empty', () => { + const message = { properties: {} }; + expect(() => { + fetchUserIds(calculateConversionObject(message)); + }).toThrow(InstrumentationError); + }); + + // Returns a conversion object with currency code 'USD' and amount 0 when message properties price is defined but quantity is 0 + it('should return a conversion object with currency code "USD" and amount 0 when message properties price is defined but quantity is 0', () => { + const message = { properties: { price: 10, quantity: 0 } }; + const conversionObject = calculateConversionObject(message); + expect(conversionObject).toEqual({ currencyCode: 'USD', amount: '0' }); + }); +}); + +describe('fetchUserIds', () => { + // Throws an InstrumentationError when no user id is found in the message and no exception is caught + it('should throw an InstrumentationError when no user id is found in the message and no exception is caught', () => { + const message = {}; + const destConfig = { + hashData: true, + }; + expect(() => { + fetchUserIds(message, destConfig); + }).toThrow(InstrumentationError); + }); + it('should create user Ids array of objects with all allowed values', () => { + const message = { + context: { + traits: { + email: 'abc@gmail.com', + }, + externalId: [ + { + type: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + id: 'abcdefg', + }, + { + type: 'ACXIOM_ID', + id: '123456', + }, + { + type: 'ORACLE_MOAT_ID', + id: '789012', + }, + ], + }, + }; + const destConfig = { + hashData: true, + }; + const userIdArray = fetchUserIds(message, destConfig); + expect(userIdArray).toEqual([ + { + idType: 'SHA256_EMAIL', + idValue: '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'abcdefg', + }, + { + idType: 'ACXIOM_ID', + idValue: '123456', + }, + { + idType: 'ORACLE_MOAT_ID', + idValue: '789012', + }, + ]); + }); +}); + +describe('curateUserInfoObject', () => { + // Returns a non-null object when given a message with both first and last name + it('should return a non-null object when given a message with both first and last name and other properties', () => { + const message = { + context: { + traits: { + firstName: 'John', + lastName: 'Doe', + title: 'Mr.', + companyName: 'RudderTest', + countryCode: 'USA', + }, + }, + }; + const result = curateUserInfoObject(message); + expect(result).toEqual({ + firstName: 'John', + lastName: 'Doe', + title: 'Mr.', + companyName: 'RudderTest', + countryCode: 'USA', + }); + }); + // Returns a null object when given a message with an empty first name + it('should return a null object when given a message without both first and last name', () => { + const message = { + context: { + traits: { + title: 'Mr.', + companyName: 'RudderTest', + countryCode: 'USA', + }, + }, + }; + const result = curateUserInfoObject(message); + expect(result).toEqual(null); + }); +}); + +describe('deduceConversionRules', () => { + // When conversionMapping is empty, return ConfigurationError + it('should return ConfigurationError when conversionMapping is empty', () => { + const trackEventName = 'eventName'; + const destConfig = { + conversionMapping: [], + }; + expect(() => deduceConversionRules(trackEventName, destConfig)).toThrow(ConfigurationError); + }); + + // When conversionMapping is not empty, return the conversion rule + it('should return the conversion rule when conversionMapping is not empty', () => { + const trackEventName = 'eventName'; + const destConfig = { + conversionMapping: [{ from: 'eventName', to: 'conversionEvent' }], + }; + const result = deduceConversionRules(trackEventName, destConfig); + expect(result).toEqual(['conversionEvent']); + }); + + it('should return the conversion rule when conversionMapping is not empty', () => { + const trackEventName = 'eventName'; + const destConfig = { + conversionMapping: [ + { from: 'eventName', to: 'conversionEvent' }, + { from: 'eventName', to: 'conversionEvent2' }, + ], + }; + const result = deduceConversionRules(trackEventName, destConfig); + expect(result).toEqual(['conversionEvent', 'conversionEvent2']); + }); +}); + +describe('generateHeader', () => { + // Returns a headers object with Content-Type, X-RestLi-Method, X-Restli-Protocol-Version, LinkedIn-Version, and Authorization keys when passed a valid access token. + it('should return a headers object with all keys when passed a valid access token', () => { + // Arrange + const accessToken = 'validAccessToken'; + + // Act + const result = generateHeader(accessToken); + + // Assert + expect(result).toEqual({ + 'Content-Type': 'application/json', + 'X-RestLi-Method': API_HEADER_METHOD, + 'X-Restli-Protocol-Version': API_PROTOCOL_VERSION, + 'LinkedIn-Version': API_VERSION, + Authorization: `Bearer ${accessToken}`, + }); + }); + + // Returns a headers object with default values for all keys when passed an invalid access token. + it('should return a headers object with default values for all keys when passed an invalid access token', () => { + // Arrange + const accessToken = 'invalidAccessToken'; + + // Act + const result = generateHeader(accessToken); + + // Assert + expect(result).toEqual({ + 'Content-Type': 'application/json', + 'X-RestLi-Method': API_HEADER_METHOD, + 'X-Restli-Protocol-Version': API_PROTOCOL_VERSION, + 'LinkedIn-Version': API_VERSION, + Authorization: `Bearer ${accessToken}`, + }); + }); +}); + +describe('constructPartialStatus', () => { + // The function correctly constructs a map of error messages when given a string containing error messages. + it('should correctly construct a map of error messages when given a string containing error messages', () => { + const errorMessage = 'Index: 1, ERROR :: Error 1\nIndex: 2, ERROR :: Error 2\n'; + const expectedErrorMap = { + 1: 'Error 1', + 2: 'Error 2', + }; + + const result = constructPartialStatus(errorMessage); + + expect(result).toEqual(expectedErrorMap); + }); + + // The function throws an error when given a non-string input. + it('should throw an error when given a non-string input', () => { + const errorMessage = 123; + const result = constructPartialStatus(errorMessage); + expect(result).toEqual({}); + }); +}); + +describe('createResponseArray', () => { + // Returns an array of objects with statusCode, metadata and error properties + it('should return an array of objects with statusCode, metadata and error properties', () => { + // Arrange + const metadata = [{ jobId: 1 }, { jobId: 2 }, { jobId: 3 }]; + const partialStatus = { + 0: 'Partial status message 1', + 2: 'Partial status message 3', + }; + + // Act + const result = createResponseArray(metadata, partialStatus); + + // Assert + expect(result).toEqual([ + { + statusCode: 400, + metadata: { jobId: 1 }, + error: 'Partial status message 1', + }, + { + statusCode: 500, + metadata: { jobId: 2 }, + error: 'success', + }, + { + statusCode: 400, + metadata: { jobId: 3 }, + error: 'Partial status message 3', + }, + ]); + }); +}); + +describe('checkIfPricePresent', () => { + // Returns true if properties object has a 'price' field + it('should return true when properties object has a price field', () => { + const properties = { price: 10 }; + const result = checkIfPricePresent(properties); + expect(result).toBe(true); + }); + + // Returns true if properties object has a 'products' array with an object containing a 'price' field and a 'price' field in the properties object + it('should return true when properties object has a products array with an object containing a price field and a price field in the properties object', () => { + const properties = { products: [{ price: 10 }, { quantity: 3 }], price: 20 }; + const result = checkIfPricePresent(properties); + expect(result).toBe(true); + }); + + // Returns false if properties object does not have a 'price' field or a 'products' array with an object containing a 'price' field + it('should return false when properties object does not have a price field or a products array with an object containing a price field', () => { + const properties = { quantity: 5 }; + const result = checkIfPricePresent(properties); + expect(result).toBe(false); + }); +}); diff --git a/src/cdk/v2/destinations/movable_ink/config.js b/src/cdk/v2/destinations/movable_ink/config.js index 673e94620e..9a0200ab44 100644 --- a/src/cdk/v2/destinations/movable_ink/config.js +++ b/src/cdk/v2/destinations/movable_ink/config.js @@ -1,3 +1,4 @@ module.exports = { - MAX_REQUEST_SIZE_IN_BYTES: 13500, + MAX_REQUEST_SIZE_IN_BYTES: 1000000, + MAX_BATCH_SIZE: 1000, }; diff --git a/src/cdk/v2/destinations/movable_ink/procWorkflow.yaml b/src/cdk/v2/destinations/movable_ink/procWorkflow.yaml index 43dbb3cbce..394190049b 100644 --- a/src/cdk/v2/destinations/movable_ink/procWorkflow.yaml +++ b/src/cdk/v2/destinations/movable_ink/procWorkflow.yaml @@ -24,6 +24,7 @@ steps: $.assertConfig(.destination.Config.accessKey, "Access key is not present . Aborting"); $.assertConfig(.destination.Config.accessSecret, "Access Secret is not present. Aborting"); $.assert(.message.timestamp ?? .message.originalTimestamp, "Timestamp is not present. Aborting"); + $.assert(!(messageType === {{$.EventType.TRACK}} && !(.message.event)), "Event name is not present. Aborting"); const userId = .message.().( {{{{$.getGenericPaths("userIdOnly")}}}}; diff --git a/src/cdk/v2/destinations/movable_ink/rtWorkflow.yaml b/src/cdk/v2/destinations/movable_ink/rtWorkflow.yaml index 46afb34d53..3ffa49f15b 100644 --- a/src/cdk/v2/destinations/movable_ink/rtWorkflow.yaml +++ b/src/cdk/v2/destinations/movable_ink/rtWorkflow.yaml @@ -42,7 +42,7 @@ steps: description: Batches the successfulEvents template: | let batches = $.BatchUtils.chunkArrayBySizeAndLength( - $.outputs.successfulEvents, {maxSizeInBytes: $.MAX_REQUEST_SIZE_IN_BYTES}).items; + $.outputs.successfulEvents, {maxSizeInBytes: $.MAX_REQUEST_SIZE_IN_BYTES, maxItems: $.MAX_BATCH_SIZE}).items; batches@batch.({ "batchedRequest": { diff --git a/src/cdk/v2/destinations/ninetailed/config.js b/src/cdk/v2/destinations/ninetailed/config.js index c38496a415..a59b2a1671 100644 --- a/src/cdk/v2/destinations/ninetailed/config.js +++ b/src/cdk/v2/destinations/ninetailed/config.js @@ -17,10 +17,6 @@ const ConfigCategories = { type: 'identify', name: 'identifyMapping', }, - PAGE: { - type: 'page', - name: 'pageMapping', - }, }; // MAX_BATCH_SIZE : // Maximum number of events to send in a single batch diff --git a/src/cdk/v2/destinations/ninetailed/data/pageMapping.json b/src/cdk/v2/destinations/ninetailed/data/pageMapping.json deleted file mode 100644 index 80ec2f58f1..0000000000 --- a/src/cdk/v2/destinations/ninetailed/data/pageMapping.json +++ /dev/null @@ -1,7 +0,0 @@ -[ - { - "sourceKeys": "properties", - "required": true, - "destKey": "properties" - } -] diff --git a/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml b/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml index 6f5056ce10..383b850a4d 100644 --- a/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml +++ b/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml @@ -16,7 +16,7 @@ steps: template: | let messageType = $.outputs.messageType; $.assert(messageType, "message Type is not present. Aborting"); - $.assert(messageType in {{$.EventType.([.TRACK,.IDENTIFY,.PAGE])}}, "message type " + messageType + " is not supported"); + $.assert(messageType in {{$.EventType.([.TRACK,.IDENTIFY])}}, "message type " + messageType + " is not supported"); $.assertConfig(.destination.Config.organisationId, "Organisation ID is not present. Aborting"); $.assertConfig(.destination.Config.environment, "Environment is not present. Aborting"); - name: preparePayload diff --git a/src/cdk/v2/destinations/ninetailed/utils.js b/src/cdk/v2/destinations/ninetailed/utils.js index b716422a0e..47b27b3b9d 100644 --- a/src/cdk/v2/destinations/ninetailed/utils.js +++ b/src/cdk/v2/destinations/ninetailed/utils.js @@ -31,12 +31,6 @@ const constructFullPayload = (message) => { config.mappingConfig[config.ConfigCategories.IDENTIFY.name], ); break; - case 'page': - typeSpecifcPayload = constructPayload( - message, - config.mappingConfig[config.ConfigCategories.PAGE.name], - ); - break; default: break; } diff --git a/src/cdk/v2/destinations/rakuten/data/propertiesMapping.json b/src/cdk/v2/destinations/rakuten/data/propertiesMapping.json index db5d36fc4d..1a69f53205 100644 --- a/src/cdk/v2/destinations/rakuten/data/propertiesMapping.json +++ b/src/cdk/v2/destinations/rakuten/data/propertiesMapping.json @@ -6,8 +6,10 @@ }, { "sourceKeys": ["properties.tr", "properties.ran_site_id", "properties.ranSiteID"], - "required": true, - "destKey": "tr" + "destKey": "tr", + "metadata": { + "defaultValue": " " + } }, { "sourceKeys": ["properties.land", "properties.land_time", "properties.landTime"], diff --git a/src/cdk/v2/destinations/reddit/procWorkflow.yaml b/src/cdk/v2/destinations/reddit/procWorkflow.yaml index 59725c1257..7b989f15e4 100644 --- a/src/cdk/v2/destinations/reddit/procWorkflow.yaml +++ b/src/cdk/v2/destinations/reddit/procWorkflow.yaml @@ -12,6 +12,7 @@ bindings: path: ../../../../v0/util/index - name: OAuthSecretError path: '@rudderstack/integrations-lib' + - path: ./utils steps: - name: validateInput @@ -56,14 +57,14 @@ steps: const event_type = (eventNames.length === 0 || eventNames[0]==="") ? ({"tracking_type": "Custom", "custom_event_name": event}): ({tracking_type: eventNames[0]}); - name: customFields - condition: $.outputs.prepareTrackPayload.eventType.tracking_type === "Purchase" + condition: $.outputs.prepareTrackPayload.eventType.tracking_type in ['Purchase', 'AddToCart', 'ViewContent'] reference: 'https://ads-api.reddit.com/docs/v2/#tag/Conversions/paths/~1api~1v2.0~1conversions~1events~1%7Baccount_id%7D/post' template: | - const revenue_in_cents = .message.properties.revenue ? Math.round(Number(.message.properties.revenue)*100) + const revenue_in_cents = $.populateRevenueField($.outputs.prepareTrackPayload.eventType.tracking_type,^.message.properties) const customFields = .message.().({ "currency": .properties.currency, "value_decimal": revenue_in_cents ? revenue_in_cents / 100, - "item_count": (Array.isArray(.properties.products) && .properties.products.length) || (.properties.itemCount && Number(.properties.itemCount)), + "item_count": $.outputs.prepareTrackPayload.eventType.tracking_type === 'Purchase' ? (Array.isArray(.properties.products) && .properties.products.length) || (.properties.itemCount && Number(.properties.itemCount)) : null, "value": revenue_in_cents, "conversion_id": .properties.conversionId || .messageId, }); diff --git a/src/cdk/v2/destinations/reddit/utils.js b/src/cdk/v2/destinations/reddit/utils.js index c108603235..f562d31313 100644 --- a/src/cdk/v2/destinations/reddit/utils.js +++ b/src/cdk/v2/destinations/reddit/utils.js @@ -28,6 +28,59 @@ const batchEvents = (successfulEvents) => { const batchedEvents = batchEventChunks(eventChunks); return batchedEvents; }; + +const calculateDefaultRevenue = (properties) => { + // Check if working with products array + if (properties?.products && properties.products.length > 0) { + // Check if all product prices are undefined + if (properties.products.every((product) => product.price === undefined)) { + return null; // Return null if all product prices are undefined + } + // Proceed with calculation if not all prices are undefined + return properties.products.reduce( + (acc, product) => acc + (product.price || 0) * (product.quantity || 1), + 0, + ); + } + // For single product scenario, check if price is undefined + if (properties.price === undefined) { + return null; // Return null if price is undefined + } + // Proceed with calculation if price is defined + return properties.price * (properties.quantity ?? 1); +}; + +const populateRevenueField = (eventType, properties) => { + let revenueInCents; + switch (eventType) { + case 'Purchase': + revenueInCents = + properties.revenue && !Number.isNaN(properties.revenue) + ? Math.round(Number(properties?.revenue) * 100) + : null; + break; + case 'AddToCart': + revenueInCents = + properties.price && !Number.isNaN(properties.price) + ? Math.round(Number(properties?.price) * Number(properties?.quantity || 1) * 100) + : null; + break; + default: + // for viewContent + // eslint-disable-next-line no-case-declarations + const revenue = calculateDefaultRevenue(properties); + revenueInCents = revenue ? revenue * 100 : null; + break; + } + + if (lodash.isNaN(revenueInCents)) { + return null; + } + // Return the value as it is if it's not NaN + return revenueInCents; +}; module.exports = { batchEvents, + populateRevenueField, + calculateDefaultRevenue, }; diff --git a/src/cdk/v2/destinations/reddit/utils.test.js b/src/cdk/v2/destinations/reddit/utils.test.js new file mode 100644 index 0000000000..7cfa87e38f --- /dev/null +++ b/src/cdk/v2/destinations/reddit/utils.test.js @@ -0,0 +1,121 @@ +const { calculateDefaultRevenue, populateRevenueField } = require('./utils'); + +describe('calculateDefaultRevenue', () => { + // Calculates revenue for a single product with defined price and quantity + it('should calculate revenue for a single product with defined price and quantity', () => { + const properties = { + price: 10, + quantity: 2, + }; + + const result = calculateDefaultRevenue(properties); + + expect(result).toBe(20); + }); + + // Returns null for properties parameter being undefined + it('should return null for price parameter being undefined', () => { + const properties = { products: [{ quantity: 1 }] }; + + const result = calculateDefaultRevenue(properties); + + expect(result).toBeNull(); + }); + + // Calculates revenue for a single product with defined price and default quantity + it('should calculate revenue for a single product with defined price and default quantity', () => { + const properties = { + price: 10, + }; + + const result = calculateDefaultRevenue(properties); + + expect(result).toBe(10); + }); + + // Calculates revenue for multiple products with defined prices and quantities + it('should calculate revenue for multiple products with defined prices and quantities', () => { + const properties = { + products: [{ price: 10, quantity: 2 }, { quantity: 3 }], + }; + + const result = calculateDefaultRevenue(properties); + + expect(result).toBe(20); + }); + + // Calculates revenue for multiple products with defined prices and default quantities + it('should calculate revenue for multiple products with defined prices and default quantities', () => { + const properties = { + products: [{ price: 10 }, { price: 5 }], + }; + + const result = calculateDefaultRevenue(properties); + + expect(result).toBe(15); + }); +}); + +describe('populateRevenueField', () => { + // Returns revenue in cents for Purchase event type with valid revenue property + it('should return revenue in cents when Purchase event type has valid revenue property', () => { + const eventType = 'Purchase'; + const properties = { + revenue: '10.50', + }; + const expected = 1050; + + const result = populateRevenueField(eventType, properties); + + expect(result).toBe(expected); + }); + + // Returns null for Purchase event type with revenue property as non-numeric string + it('should return null when Purchase event type has revenue property as non-numeric string', () => { + const eventType = 'Purchase'; + const properties = { + revenue: 'invalid', + }; + const expected = null; + + const result = populateRevenueField(eventType, properties); + + expect(result).toBe(expected); + }); + + // Returns revenue in cents for AddToCart event type with valid price and quantity properties + it('should return revenue in cents when AddToCart event type has valid price and quantity properties', () => { + const eventType = 'AddToCart'; + const properties = { + price: '10.50', + quantity: 2, + }; + const expected = 2100; + + const result = populateRevenueField(eventType, properties); + + expect(result).toBe(expected); + }); + + // Returns revenue in cents for ViewContent event type with valid properties + it('should return revenue in cents when ViewContent event type has valid properties', () => { + const eventType = 'ViewContent'; + const properties = { + products: [ + { + price: '10.50', + quantity: 2, + }, + { + price: '5.25', + quantity: 3, + }, + ], + }; + const expected = 3675; + + const result = populateRevenueField(eventType, properties); + + expect(result).toBe(expected); + }); +}); diff --git a/src/cdk/v2/handler.ts b/src/cdk/v2/handler.ts index edd14e7298..c437247f74 100644 --- a/src/cdk/v2/handler.ts +++ b/src/cdk/v2/handler.ts @@ -1,9 +1,9 @@ import { - WorkflowEngine, - WorkflowEngineFactory, - TemplateType, ExecutionBindings, StepOutput, + TemplateType, + WorkflowEngine, + WorkflowEngineFactory, } from '@rudderstack/workflow-engine'; import { FixMe } from '../../util/types'; @@ -11,9 +11,9 @@ import tags from '../../v0/util/tags'; import { getErrorInfo, + getPlatformBindingsPaths, getRootPathForDestination, getWorkflowPath, - getPlatformBindingsPaths, isCdkV2Destination, } from './utils'; @@ -82,10 +82,12 @@ export async function processCdkV2Workflow( destType: string, parsedEvent: FixMe, feature: string, + logger: FixMe, requestMetadata: NonNullable = {}, bindings: Record = {}, ) { try { + logger.debug(`Processing cdkV2 workflow`); const workflowEngine = await getCachedWorkflowEngine(destType, feature, bindings); return await executeWorkflow(workflowEngine, parsedEvent, requestMetadata); } catch (error) { diff --git a/src/constants/destinationCanonicalNames.js b/src/constants/destinationCanonicalNames.js index b84aff1089..ee4f4f0b33 100644 --- a/src/constants/destinationCanonicalNames.js +++ b/src/constants/destinationCanonicalNames.js @@ -165,6 +165,7 @@ const DestCanonicalNames = { 'google adwords offline conversions', ], koala: ['Koala', 'koala', 'KOALA'], + bloomreach: ['Bloomreach', 'bloomreach', 'BLOOMREACH'], }; module.exports = { DestHandlerMap, DestCanonicalNames }; diff --git a/src/controllers/bulkUpload.ts b/src/controllers/bulkUpload.ts index dbd77dc07f..28556dd5df 100644 --- a/src/controllers/bulkUpload.ts +++ b/src/controllers/bulkUpload.ts @@ -1,10 +1,10 @@ /* eslint-disable global-require, import/no-dynamic-require, @typescript-eslint/no-unused-vars */ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import { client as errNotificationClient } from '../util/errorNotifier'; -import logger from '../logger'; import { + getDestFileUploadHandler, getJobStatusHandler, getPollStatusHandler, - getDestFileUploadHandler, } from '../util/fetchDestinationHandlers'; import { CatchErr, ContextBodySimple } from '../util/types'; // TODO: To be refactored and redisgned @@ -31,10 +31,7 @@ const getReqMetadata = (ctx) => { }; export const fileUpload = async (ctx) => { - logger.debug( - 'Native(Bulk-Upload): Request to transformer:: /fileUpload route', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Bulk-Upload): Request to transformer:: /fileUpload route', ctx.request.body); const getReqMetadataFileUpload = () => { try { const reqBody = ctx.request.body; @@ -69,18 +66,12 @@ export const fileUpload = async (ctx) => { }); } ctx.body = response; - logger.debug( - 'Native(Bulk-Upload): Response from transformer:: /fileUpload route', - JSON.stringify(ctx.body), - ); + logger.debug('Native(Bulk-Upload): Response from transformer:: /fileUpload route', ctx.body); return ctx.body; }; export const pollStatus = async (ctx) => { - logger.debug( - 'Native(Bulk-Upload): Request to transformer:: /pollStatus route', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Bulk-Upload): Request to transformer:: /pollStatus route', ctx.request.body); const { destType }: ContextBodySimple = ctx.request.body; const destFileUploadHandler = getPollStatusHandler('v0', destType.toLowerCase()); @@ -104,17 +95,14 @@ export const pollStatus = async (ctx) => { }); } ctx.body = response; - logger.debug( - 'Native(Bulk-Upload): Request from transformer:: /pollStatus route', - JSON.stringify(ctx.body), - ); + logger.debug('Native(Bulk-Upload): Request from transformer:: /pollStatus route', ctx.body); return ctx.body; }; export const getWarnJobStatus = async (ctx) => { logger.debug( 'Native(Bulk-Upload): Request to transformer:: /getWarningJobs route', - JSON.stringify(ctx.request.body), + ctx.request.body, ); const { destType }: ContextBodySimple = ctx.request.body; @@ -140,17 +128,14 @@ export const getWarnJobStatus = async (ctx) => { }); } ctx.body = response; - logger.debug( - 'Native(Bulk-Upload): Request from transformer:: /getWarningJobs route', - JSON.stringify(ctx.body), - ); + logger.debug('Native(Bulk-Upload): Request from transformer:: /getWarningJobs route', ctx.body); return ctx.body; }; export const getFailedJobStatus = async (ctx) => { logger.debug( 'Native(Bulk-Upload): Request to transformer:: /getFailedJobs route', - JSON.stringify(ctx.request.body), + ctx.request.body, ); const { destType }: ContextBodySimple = ctx.request.body; @@ -176,9 +161,6 @@ export const getFailedJobStatus = async (ctx) => { }); } ctx.body = response; - logger.debug( - 'Native(Bulk-Upload): Request from transformer:: /getFailedJobs route', - JSON.stringify(ctx.body), - ); + logger.debug('Native(Bulk-Upload): Request from transformer:: /getFailedJobs route', ctx.body); return ctx.body; }; diff --git a/src/controllers/delivery.ts b/src/controllers/delivery.ts index 4334dc33b2..0dc27553cb 100644 --- a/src/controllers/delivery.ts +++ b/src/controllers/delivery.ts @@ -1,28 +1,30 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable sonarjs/no-duplicate-string */ +import { + isDefinedAndNotNullAndNotEmpty, + structuredLogger as logger, +} from '@rudderstack/integrations-lib'; import { Context } from 'koa'; -import { isDefinedAndNotNullAndNotEmpty } from '@rudderstack/integrations-lib'; +import { ServiceSelector } from '../helpers/serviceSelector'; +import { DeliveryTestService } from '../services/delivertTest/deliveryTest'; +import { DestinationPostTransformationService } from '../services/destination/postTransformation'; import { MiscService } from '../services/misc'; import { - DeliveryV1Response, DeliveryV0Response, + DeliveryV1Response, ProcessorTransformationOutput, ProxyV0Request, ProxyV1Request, } from '../types/index'; -import { ServiceSelector } from '../helpers/serviceSelector'; -import { DeliveryTestService } from '../services/delivertTest/deliveryTest'; -import { ControllerUtility } from './util'; -import logger from '../logger'; -import { DestinationPostTransformationService } from '../services/destination/postTransformation'; -import tags from '../v0/util/tags'; import { FixMe } from '../util/types'; +import tags from '../v0/util/tags'; +import { ControllerUtility } from './util'; const NON_DETERMINABLE = 'Non-determinable'; export class DeliveryController { public static async deliverToDestination(ctx: Context) { - logger.debug('Native(Delivery):: Request to transformer::', JSON.stringify(ctx.request.body)); + logger.debug('Native(Delivery):: Request to transformer::', ctx.request.body); let deliveryResponse: DeliveryV0Response; const requestMetadata = MiscService.getRequestMetadata(ctx); const deliveryRequest = ctx.request.body as ProxyV0Request; @@ -52,12 +54,12 @@ export class DeliveryController { ctx.body = { output: deliveryResponse }; ControllerUtility.deliveryPostProcess(ctx, deliveryResponse.status); - logger.debug('Native(Delivery):: Response from transformer::', JSON.stringify(ctx.body)); + logger.debug('Native(Delivery):: Response from transformer::', ctx.body); return ctx; } public static async deliverToDestinationV1(ctx: Context) { - logger.debug('Native(Delivery):: Request to transformer::', JSON.stringify(ctx.request.body)); + logger.debug('Native(Delivery):: Request to transformer::', ctx.request.body); let deliveryResponse: DeliveryV1Response; const requestMetadata = MiscService.getRequestMetadata(ctx); const deliveryRequest = ctx.request.body as ProxyV1Request; @@ -91,15 +93,12 @@ export class DeliveryController { ControllerUtility.deliveryPostProcess(ctx); } - logger.debug('Native(Delivery):: Response from transformer::', JSON.stringify(ctx.body)); + logger.debug('Native(Delivery):: Response from transformer::', ctx.body); return ctx; } public static async testDestinationDelivery(ctx: Context) { - logger.debug( - 'Native(Delivery-Test):: Request to transformer::', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Delivery-Test):: Request to transformer::', ctx.request.body); const { destination }: { destination: string } = ctx.params; const { version }: { version: string } = ctx.params; const { @@ -117,7 +116,7 @@ export class DeliveryController { ); ctx.body = { output: response }; ControllerUtility.postProcess(ctx); - logger.debug('Native(Delivery-Test):: Response from transformer::', JSON.stringify(ctx.body)); + logger.debug('Native(Delivery-Test):: Response from transformer::', ctx.body); return ctx; } } diff --git a/src/controllers/destination.ts b/src/controllers/destination.ts index d8b3c94524..92ef4b4c19 100644 --- a/src/controllers/destination.ts +++ b/src/controllers/destination.ts @@ -1,29 +1,27 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import { Context } from 'koa'; -import { MiscService } from '../services/misc'; -import { DestinationPreTransformationService } from '../services/destination/preTransformation'; +import { ServiceSelector } from '../helpers/serviceSelector'; import { DestinationPostTransformationService } from '../services/destination/postTransformation'; +import { DestinationPreTransformationService } from '../services/destination/preTransformation'; +import { MiscService } from '../services/misc'; import { + ErrorDetailer, ProcessorTransformationRequest, - RouterTransformationRequest, ProcessorTransformationResponse, + RouterTransformationRequest, RouterTransformationResponse, } from '../types/index'; -import { ServiceSelector } from '../helpers/serviceSelector'; -import { ControllerUtility } from './util'; +import { DynamicConfigParser } from '../util/dynamicConfigParser'; import stats from '../util/stats'; -import logger from '../logger'; import { getIntegrationVersion } from '../util/utils'; -import tags from '../v0/util/tags'; -import { DynamicConfigParser } from '../util/dynamicConfigParser'; import { checkInvalidRtTfEvents } from '../v0/util'; +import tags from '../v0/util/tags'; +import { ControllerUtility } from './util'; export class DestinationController { public static async destinationTransformAtProcessor(ctx: Context) { const startTime = new Date(); - logger.debug( - 'Native(Process-Transform):: Requst to transformer::', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Process-Transform):: Requst to transformer::', ctx.request.body); let resplist: ProcessorTransformationResponse[]; const requestMetadata = MiscService.getRequestMetadata(ctx); let events = ctx.request.body as ProcessorTransformationRequest[]; @@ -35,6 +33,9 @@ export class DestinationController { ...metaTags, }); const integrationService = ServiceSelector.getDestinationService(events); + const loggerWithCtx = logger.child({ + ...MiscService.getLoggableData(events[0]?.metadata as unknown as ErrorDetailer), + }); try { integrationService.init(); events = DestinationPreTransformationService.preProcess( @@ -50,6 +51,7 @@ export class DestinationController { destination, version, requestMetadata, + loggerWithCtx, ); } catch (error: any) { resplist = events.map((ev) => { @@ -69,10 +71,7 @@ export class DestinationController { } ctx.body = resplist; ControllerUtility.postProcess(ctx); - logger.debug( - 'Native(Process-Transform):: Response from transformer::', - JSON.stringify(ctx.body), - ); + loggerWithCtx.debug('Native(Process-Transform):: Response from transformer::', ctx.body); stats.histogram('dest_transform_output_events', resplist.length, { destination, version, @@ -94,10 +93,7 @@ export class DestinationController { public static async destinationTransformAtRouter(ctx: Context) { const startTime = new Date(); - logger.debug( - 'Native(Router-Transform):: Requst to transformer::', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Router-Transform):: Requst to transformer::', ctx.request.body); const requestMetadata = MiscService.getRequestMetadata(ctx); const routerRequest = ctx.request.body as RouterTransformationRequest; const destination = routerRequest.destType; @@ -117,6 +113,9 @@ export class DestinationController { return ctx; } const metaTags = MiscService.getMetaTags(events[0].metadata); + const loggerWithCtx = logger.child({ + ...MiscService.getLoggableData(events[0]?.metadata as unknown as ErrorDetailer), + }); stats.histogram('dest_transform_input_events', events.length, { destination, version: 'v0', @@ -133,6 +132,7 @@ export class DestinationController { destination, getIntegrationVersion(), requestMetadata, + loggerWithCtx, ); } catch (error: any) { const metaTO = integrationService.getTags( @@ -155,10 +155,7 @@ export class DestinationController { version: 'v0', ...metaTags, }); - logger.debug( - 'Native(Router-Transform):: Response from transformer::', - JSON.stringify(ctx.body), - ); + loggerWithCtx.debug('Native(Router-Transform):: Response from transformer::', ctx.body); stats.timing('dest_transform_request_latency', startTime, { destination, version: 'v0', @@ -169,15 +166,15 @@ export class DestinationController { } public static batchProcess(ctx: Context) { - logger.debug( - 'Native(Process-Transform-Batch):: Requst to transformer::', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Process-Transform-Batch):: Requst to transformer::', ctx.request.body); const startTime = new Date(); const requestMetadata = MiscService.getRequestMetadata(ctx); const routerRequest = ctx.request.body as RouterTransformationRequest; const destination = routerRequest.destType; let events = routerRequest.input; + const loggerWithCtx = logger.child({ + ...MiscService.getLoggableData(events[0]?.metadata as unknown as ErrorDetailer), + }); const integrationService = ServiceSelector.getDestinationService(events); try { events = DestinationPreTransformationService.preProcess(events, ctx); @@ -187,6 +184,7 @@ export class DestinationController { destination, getIntegrationVersion(), requestMetadata, + loggerWithCtx, ); ctx.body = resplist; } catch (error: any) { @@ -204,10 +202,7 @@ export class DestinationController { ctx.body = [errResp]; } ControllerUtility.postProcess(ctx); - logger.debug( - 'Native(Process-Transform-Batch):: Response from transformer::', - JSON.stringify(ctx.body), - ); + loggerWithCtx.debug('Native(Process-Transform-Batch):: Response from transformer::', ctx.body); stats.timing('dest_transform_request_latency', startTime, { destination, feature: tags.FEATURES.BATCH, diff --git a/src/controllers/regulation.ts b/src/controllers/regulation.ts index 318b5ed4e7..4b8f87e3fa 100644 --- a/src/controllers/regulation.ts +++ b/src/controllers/regulation.ts @@ -1,19 +1,16 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import { Context } from 'koa'; -import logger from '../logger'; -import { UserDeletionRequest, UserDeletionResponse } from '../types'; import { ServiceSelector } from '../helpers/serviceSelector'; -import tags from '../v0/util/tags'; -import stats from '../util/stats'; import { DestinationPostTransformationService } from '../services/destination/postTransformation'; +import { UserDeletionRequest, UserDeletionResponse } from '../types'; +import stats from '../util/stats'; +import tags from '../v0/util/tags'; // eslint-disable-next-line @typescript-eslint/no-unused-vars import { CatchErr } from '../util/types'; export class RegulationController { public static async deleteUsers(ctx: Context) { - logger.debug( - 'Native(Process-Transform):: Requst to transformer::', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Process-Transform):: Requst to transformer::', ctx.request.body); const startTime = new Date(); let rudderDestInfo: any; try { diff --git a/src/controllers/source.ts b/src/controllers/source.ts index ef5483a756..e1a4931371 100644 --- a/src/controllers/source.ts +++ b/src/controllers/source.ts @@ -1,20 +1,18 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import { Context } from 'koa'; -import { MiscService } from '../services/misc'; import { ServiceSelector } from '../helpers/serviceSelector'; -import { ControllerUtility } from './util'; -import logger from '../logger'; +import { MiscService } from '../services/misc'; import { SourcePostTransformationService } from '../services/source/postTransformation'; +import { ControllerUtility } from './util'; export class SourceController { public static async sourceTransform(ctx: Context) { - logger.debug( - 'Native(Source-Transform):: Request to transformer::', - JSON.stringify(ctx.request.body), - ); + logger.debug('Native(Source-Transform):: Request to transformer::', ctx.request.body); const requestMetadata = MiscService.getRequestMetadata(ctx); const events = ctx.request.body as object[]; const { version, source }: { version: string; source: string } = ctx.params; const integrationService = ServiceSelector.getNativeSourceService(); + const loggerWithCtx = logger.child({ version, source }); try { const { implementationVersion, input } = ControllerUtility.adaptInputToVersion( source, @@ -26,6 +24,7 @@ export class SourceController { source, implementationVersion, requestMetadata, + loggerWithCtx, ); ctx.body = resplist; } catch (err: any) { @@ -34,10 +33,7 @@ export class SourceController { ctx.body = [resp]; } ControllerUtility.postProcess(ctx); - logger.debug( - 'Native(Source-Transform):: Response from transformer::', - JSON.stringify(ctx.body), - ); + loggerWithCtx.debug('Native(Source-Transform):: Response from transformer::', ctx.body); return ctx; } } diff --git a/src/controllers/userTransform.ts b/src/controllers/userTransform.ts index 3e01686a52..0e288c6f04 100644 --- a/src/controllers/userTransform.ts +++ b/src/controllers/userTransform.ts @@ -1,10 +1,10 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import { Context } from 'koa'; -import { ProcessorTransformationRequest, UserTransformationServiceResponse } from '../types/index'; import { UserTransformService } from '../services/userTransform'; -import logger from '../logger'; +import { ProcessorTransformationRequest, UserTransformationServiceResponse } from '../types/index'; import { - setupUserTransformHandler, extractLibraries, + setupUserTransformHandler, validateCode, } from '../util/customTransformer'; import { ControllerUtility } from './util'; @@ -13,7 +13,7 @@ export class UserTransformController { public static async transform(ctx: Context) { logger.debug( '(User transform - router:/customTransform ):: Request to transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); const requestSize = Number(ctx.request.get('content-length')); const events = ctx.request.body as ProcessorTransformationRequest[]; @@ -23,7 +23,7 @@ export class UserTransformController { ControllerUtility.postProcess(ctx, processedRespone.retryStatus); logger.debug( '(User transform - router:/customTransform ):: Response from transformer', - JSON.stringify(ctx.response.body), + ctx.response.body, ); return ctx; } @@ -31,7 +31,7 @@ export class UserTransformController { public static async testTransform(ctx: Context) { logger.debug( '(User transform - router:/transformation/test ):: Request to transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); const { events, trRevCode, libraryVersionIDs = [] } = ctx.request.body as any; const response = await UserTransformService.testTransformRoutine( @@ -43,7 +43,7 @@ export class UserTransformController { ControllerUtility.postProcess(ctx, response.status); logger.debug( '(User transform - router:/transformation/test ):: Response from transformer', - JSON.stringify(ctx.response.body), + ctx.response.body, ); return ctx; } @@ -51,7 +51,7 @@ export class UserTransformController { public static async testTransformLibrary(ctx: Context) { logger.debug( '(User transform - router:/transformationLibrary/test ):: Request to transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); try { const { code, language = 'javascript' } = ctx.request.body as any; @@ -66,7 +66,7 @@ export class UserTransformController { } logger.debug( '(User transform - router:/transformationLibrary/test ):: Response from transformer', - JSON.stringify(ctx.response.body), + ctx.response.body, ); return ctx; } @@ -74,7 +74,7 @@ export class UserTransformController { public static async testTransformSethandle(ctx: Context) { logger.debug( '(User transform - router:/transformation/sethandle ):: Request to transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); try { const { trRevCode, libraryVersionIDs = [] } = ctx.request.body as any; @@ -96,7 +96,7 @@ export class UserTransformController { } logger.debug( '(User transform - router:/transformation/sethandle ):: Response from transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); return ctx; } @@ -104,7 +104,7 @@ export class UserTransformController { public static async extractLibhandle(ctx: Context) { logger.debug( '(User transform - router:/extractLibs ):: Request to transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); try { const { @@ -134,7 +134,7 @@ export class UserTransformController { } logger.debug( '(User transform - router:/extractLibs ):: Response from transformer', - JSON.stringify(ctx.request.body), + ctx.request.body, ); return ctx; } diff --git a/src/features.json b/src/features.json index 267923fdb4..6d2cac9340 100644 --- a/src/features.json +++ b/src/features.json @@ -67,8 +67,10 @@ "THE_TRADE_DESK": true, "INTERCOM": true, "NINETAILED": true, - "MOVABLE_INK": true, - "KOALA": true + "KOALA": true, + "LINKEDIN_ADS": true, + "BLOOMREACH": true, + "MOVABLE_INK": true }, "regulations": [ "BRAZE", diff --git a/src/index.ts b/src/index.ts index 36f32f1aed..5557994b2e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,14 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; +import dotenv from 'dotenv'; +import gracefulShutdown from 'http-graceful-shutdown'; import Koa from 'koa'; import bodyParser from 'koa-bodyparser'; -import gracefulShutdown from 'http-graceful-shutdown'; -import dotenv from 'dotenv'; -import logger from './logger'; -import cluster from './util/cluster'; +import { addRequestSizeMiddleware, addStatMiddleware, initPyroscope } from './middleware'; +import { addSwaggerRoutes, applicationRoutes } from './routes'; import { metricsRouter } from './routes/metricsRouter'; -import { addStatMiddleware, addRequestSizeMiddleware, initPyroscope } from './middleware'; -import { logProcessInfo } from './util/utils'; -import { applicationRoutes, addSwaggerRoutes } from './routes'; +import cluster from './util/cluster'; import { RedisDB } from './util/redis/redisConnector'; +import { logProcessInfo } from './util/utils'; dotenv.config(); const clusterEnabled = process.env.CLUSTER_ENABLED !== 'false'; diff --git a/src/interfaces/DestinationService.ts b/src/interfaces/DestinationService.ts index 4947089b5d..5d7596dac5 100644 --- a/src/interfaces/DestinationService.ts +++ b/src/interfaces/DestinationService.ts @@ -1,14 +1,14 @@ import { DeliveryV0Response, + DeliveryV1Response, MetaTransferObject, ProcessorTransformationRequest, ProcessorTransformationResponse, + ProxyRequest, RouterTransformationRequestData, RouterTransformationResponse, UserDeletionRequest, UserDeletionResponse, - ProxyRequest, - DeliveryV1Response, } from '../types/index'; export interface DestinationService { @@ -28,6 +28,7 @@ export interface DestinationService { destinationType: string, version: string, requestMetadata: NonNullable, + logger: NonNullable, ): Promise; doRouterTransformation( @@ -35,6 +36,7 @@ export interface DestinationService { destinationType: string, version: string, requestMetadata: NonNullable, + logger: NonNullable, ): Promise; doBatchTransformation( @@ -42,6 +44,7 @@ export interface DestinationService { destinationType: string, version: string, requestMetadata: NonNullable, + logger: NonNullable, ): RouterTransformationResponse[]; deliver( diff --git a/src/interfaces/SourceService.ts b/src/interfaces/SourceService.ts index c7de8cfe8b..fab6490264 100644 --- a/src/interfaces/SourceService.ts +++ b/src/interfaces/SourceService.ts @@ -1,4 +1,5 @@ import { MetaTransferObject, SourceTransformationResponse } from '../types/index'; +import { FixMe } from '../util/types'; export interface SourceService { getTags(): MetaTransferObject; @@ -8,5 +9,6 @@ export interface SourceService { sourceType: string, version: string, requestMetadata: NonNullable, + logger: FixMe, ): Promise; } diff --git a/src/middleware.js b/src/middleware.js index 53aabc90e3..543b3af8d1 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,4 +1,5 @@ const Pyroscope = require('@pyroscope/nodejs'); +const { getDestTypeFromContext } = require('@rudderstack/integrations-lib'); const stats = require('./util/stats'); function initPyroscope() { @@ -26,6 +27,7 @@ function durationMiddleware() { method: ctx.method, code: ctx.status, route: ctx.request.url, + destType: getDestTypeFromContext(ctx), }; stats.timing('http_request_duration', startTime, labels); }; diff --git a/src/services/comparator.ts b/src/services/comparator.ts index 36cb0ebd5a..511436dfd1 100644 --- a/src/services/comparator.ts +++ b/src/services/comparator.ts @@ -1,4 +1,5 @@ /* eslint-disable class-methods-use-this */ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import { DestinationService } from '../interfaces/DestinationService'; import { DeliveryV0Response, @@ -14,10 +15,9 @@ import { UserDeletionRequest, UserDeletionResponse, } from '../types'; -import tags from '../v0/util/tags'; -import stats from '../util/stats'; -import logger from '../logger'; import { CommonUtils } from '../util/common'; +import stats from '../util/stats'; +import tags from '../v0/util/tags'; const NS_PER_SEC = 1e9; @@ -204,6 +204,7 @@ export class ComparatorService implements DestinationService { destinationType, version, requestMetadata, + logger, ); const primaryTimeDiff = process.hrtime(primaryStartTime); const primaryTime = primaryTimeDiff[0] * NS_PER_SEC + primaryTimeDiff[1]; @@ -262,6 +263,7 @@ export class ComparatorService implements DestinationService { destinationType, version, requestMetadata, + logger, ); const primaryTimeDiff = process.hrtime(primaryStartTime); const primaryTime = primaryTimeDiff[0] * NS_PER_SEC + primaryTimeDiff[1]; @@ -320,6 +322,7 @@ export class ComparatorService implements DestinationService { destinationType, version, requestMetadata, + {}, ); const primaryTimeDiff = process.hrtime(primaryStartTime); const primaryTime = primaryTimeDiff[0] * NS_PER_SEC + primaryTimeDiff[1]; diff --git a/src/services/destination/__tests__/nativeIntegration.test.ts b/src/services/destination/__tests__/nativeIntegration.test.ts index 59c8b41881..85d099d292 100644 --- a/src/services/destination/__tests__/nativeIntegration.test.ts +++ b/src/services/destination/__tests__/nativeIntegration.test.ts @@ -1,11 +1,12 @@ -import { NativeIntegrationDestinationService } from '../nativeIntegration'; -import { DestinationPostTransformationService } from '../postTransformation'; +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; +import { FetchHandler } from '../../../helpers/fetchHandlers'; import { - ProcessorTransformationRequest, ProcessorTransformationOutput, + ProcessorTransformationRequest, ProcessorTransformationResponse, } from '../../../types/index'; -import { FetchHandler } from '../../../helpers/fetchHandlers'; +import { NativeIntegrationDestinationService } from '../nativeIntegration'; +import { DestinationPostTransformationService } from '../postTransformation'; afterEach(() => { jest.clearAllMocks(); @@ -47,6 +48,7 @@ describe('NativeIntegration Service', () => { destType, version, requestMetadata, + logger, ); expect(resp).toEqual(tresponse); @@ -77,6 +79,7 @@ describe('NativeIntegration Service', () => { destType, version, requestMetadata, + logger, ); const expected = [ diff --git a/src/services/destination/cdkV2Integration.ts b/src/services/destination/cdkV2Integration.ts index c18a5cd936..a649da9154 100644 --- a/src/services/destination/cdkV2Integration.ts +++ b/src/services/destination/cdkV2Integration.ts @@ -1,27 +1,28 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable class-methods-use-this */ -import groupBy from 'lodash/groupBy'; import { TransformationError } from '@rudderstack/integrations-lib'; +import groupBy from 'lodash/groupBy'; import { processCdkV2Workflow } from '../../cdk/v2/handler'; import { DestinationService } from '../../interfaces/DestinationService'; import { DeliveryV0Response, + DeliveryV1Response, ErrorDetailer, MetaTransferObject, + ProcessorTransformationOutput, ProcessorTransformationRequest, ProcessorTransformationResponse, + ProxyRequest, RouterTransformationRequestData, RouterTransformationResponse, - ProcessorTransformationOutput, UserDeletionRequest, UserDeletionResponse, - ProxyRequest, - DeliveryV1Response, } from '../../types/index'; +import stats from '../../util/stats'; +import { CatchErr, FixMe } from '../../util/types'; import tags from '../../v0/util/tags'; +import { MiscService } from '../misc'; import { DestinationPostTransformationService } from './postTransformation'; -import stats from '../../util/stats'; -import { CatchErr } from '../../util/types'; export class CDKV2DestinationService implements DestinationService { public init() {} @@ -55,10 +56,19 @@ export class CDKV2DestinationService implements DestinationService { destinationType: string, _version: string, requestMetadata: NonNullable, + logger: any, ): Promise { // TODO: Change the promise type const respList: ProcessorTransformationResponse[][] = await Promise.all( events.map(async (event) => { + const metaTo = this.getTags( + destinationType, + event.metadata.destinationId, + event.metadata.workspaceId, + tags.FEATURES.PROCESSOR, + ); + metaTo.metadata = event.metadata; + const loggerWithCtx = logger.child({ ...MiscService.getLoggableData(metaTo.errorDetails) }); try { const transformedPayloads: | ProcessorTransformationOutput @@ -66,9 +76,9 @@ export class CDKV2DestinationService implements DestinationService { destinationType, event, tags.FEATURES.PROCESSOR, + loggerWithCtx, requestMetadata, ); - stats.increment('event_transform_success', { destType: destinationType, module: tags.MODULES.DESTINATION, @@ -85,13 +95,6 @@ export class CDKV2DestinationService implements DestinationService { undefined, ); } catch (error: CatchErr) { - const metaTo = this.getTags( - destinationType, - event.metadata.destinationId, - event.metadata.workspaceId, - tags.FEATURES.PROCESSOR, - ); - metaTo.metadata = event.metadata; const erroredResp = DestinationPostTransformationService.handleProcessorTransformFailureEvents( error, @@ -112,6 +115,7 @@ export class CDKV2DestinationService implements DestinationService { destinationType: string, _version: string, requestMetadata: NonNullable, + logger: FixMe, ): Promise { const allDestEvents: object = groupBy( events, @@ -127,12 +131,16 @@ export class CDKV2DestinationService implements DestinationService { tags.FEATURES.ROUTER, ); metaTo.metadata = destInputArray[0].metadata; + const loggerWithCtx = logger.child({ + ...MiscService.getLoggableData(metaTo.errorDetails), + }); try { const doRouterTransformationResponse: RouterTransformationResponse[] = await processCdkV2Workflow( destinationType, destInputArray, tags.FEATURES.ROUTER, + loggerWithCtx, requestMetadata, ); return DestinationPostTransformationService.handleRouterTransformSuccessEvents( diff --git a/src/services/destination/nativeIntegration.ts b/src/services/destination/nativeIntegration.ts index 2bb82fc602..0bc9308fcd 100644 --- a/src/services/destination/nativeIntegration.ts +++ b/src/services/destination/nativeIntegration.ts @@ -1,31 +1,32 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable sonarjs/no-duplicate-string */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import groupBy from 'lodash/groupBy'; import cloneDeep from 'lodash/cloneDeep'; +import groupBy from 'lodash/groupBy'; +import networkHandlerFactory from '../../adapters/networkHandlerFactory'; +import { FetchHandler } from '../../helpers/fetchHandlers'; import { DestinationService } from '../../interfaces/DestinationService'; import { + DeliveryJobState, DeliveryV0Response, + DeliveryV1Response, ErrorDetailer, MetaTransferObject, + ProcessorTransformationOutput, ProcessorTransformationRequest, ProcessorTransformationResponse, + ProxyRequest, + ProxyV0Request, + ProxyV1Request, RouterTransformationRequestData, RouterTransformationResponse, - ProcessorTransformationOutput, UserDeletionRequest, UserDeletionResponse, - ProxyRequest, - ProxyV0Request, - ProxyV1Request, - DeliveryV1Response, - DeliveryJobState, } from '../../types/index'; -import { DestinationPostTransformationService } from './postTransformation'; -import networkHandlerFactory from '../../adapters/networkHandlerFactory'; -import { FetchHandler } from '../../helpers/fetchHandlers'; -import tags from '../../v0/util/tags'; import stats from '../../util/stats'; +import tags from '../../v0/util/tags'; +import { MiscService } from '../misc'; +import { DestinationPostTransformationService } from './postTransformation'; export class NativeIntegrationDestinationService implements DestinationService { public init() {} @@ -59,27 +60,33 @@ export class NativeIntegrationDestinationService implements DestinationService { destinationType: string, version: string, requestMetadata: NonNullable, + logger: any, ): Promise { const destHandler = FetchHandler.getDestHandler(destinationType, version); const respList: ProcessorTransformationResponse[][] = await Promise.all( events.map(async (event) => { + const metaTO = this.getTags( + destinationType, + event.metadata?.destinationId, + event.metadata?.workspaceId, + tags.FEATURES.PROCESSOR, + ); + metaTO.metadata = event.metadata; + const loggerWithCtx = logger.child({ ...MiscService.getLoggableData(metaTO.errorDetails) }); try { const transformedPayloads: | ProcessorTransformationOutput - | ProcessorTransformationOutput[] = await destHandler.process(event, requestMetadata); + | ProcessorTransformationOutput[] = await destHandler.process( + event, + requestMetadata, + loggerWithCtx, + ); return DestinationPostTransformationService.handleProcessorTransformSucessEvents( event, transformedPayloads, destHandler, ); } catch (error: any) { - const metaTO = this.getTags( - destinationType, - event.metadata?.destinationId, - event.metadata?.workspaceId, - tags.FEATURES.PROCESSOR, - ); - metaTO.metadata = event.metadata; const erroredResp = DestinationPostTransformationService.handleProcessorTransformFailureEvents( error, @@ -97,6 +104,7 @@ export class NativeIntegrationDestinationService implements DestinationService { destinationType: string, version: string, requestMetadata: NonNullable, + logger: any, ): Promise { const destHandler = FetchHandler.getDestHandler(destinationType, version); const allDestEvents: NonNullable = groupBy( @@ -112,9 +120,16 @@ export class NativeIntegrationDestinationService implements DestinationService { destInputArray[0].metadata?.workspaceId, tags.FEATURES.ROUTER, ); + const loggerWithCtx = logger.child({ + ...MiscService.getLoggableData(metaTO.errorDetails), + }); try { const doRouterTransformationResponse: RouterTransformationResponse[] = - await destHandler.processRouterDest(cloneDeep(destInputArray), requestMetadata); + await destHandler.processRouterDest( + cloneDeep(destInputArray), + requestMetadata, + loggerWithCtx, + ); metaTO.metadata = destInputArray[0].metadata; return DestinationPostTransformationService.handleRouterTransformSuccessEvents( doRouterTransformationResponse, @@ -141,6 +156,7 @@ export class NativeIntegrationDestinationService implements DestinationService { destinationType: string, version: any, requestMetadata: NonNullable, + logger: any, ): RouterTransformationResponse[] { const destHandler = FetchHandler.getDestHandler(destinationType, version); if (!destHandler.batch) { @@ -152,20 +168,24 @@ export class NativeIntegrationDestinationService implements DestinationService { ); const groupedEvents: RouterTransformationRequestData[][] = Object.values(allDestEvents); const response = groupedEvents.map((destEvents) => { + const metaTO = this.getTags( + destinationType, + destEvents[0].metadata.destinationId, + destEvents[0].metadata.workspaceId, + tags.FEATURES.BATCH, + ); + metaTO.metadatas = events.map((event) => event.metadata); + const loggerWithCtx = logger.child({ + ...MiscService.getLoggableData(metaTO.errorDetails), + }); try { const destBatchedRequests: RouterTransformationResponse[] = destHandler.batch( destEvents, requestMetadata, + loggerWithCtx, ); return destBatchedRequests; } catch (error: any) { - const metaTO = this.getTags( - destinationType, - destEvents[0].metadata.destinationId, - destEvents[0].metadata.workspaceId, - tags.FEATURES.BATCH, - ); - metaTO.metadatas = events.map((event) => event.metadata); const errResp = DestinationPostTransformationService.handleBatchTransformFailureEvents( error, metaTO, @@ -264,6 +284,7 @@ export class NativeIntegrationDestinationService implements DestinationService { error: `${destType}: Doesn't support deletion of users`, } as UserDeletionResponse; } + const metaTO = this.getTags(destType, 'unknown', 'unknown', tags.FEATURES.USER_DELETION); try { const result: UserDeletionResponse = await destUserDeletionHandler.processDeleteUsers({ ...request, @@ -276,7 +297,6 @@ export class NativeIntegrationDestinationService implements DestinationService { }); return result; } catch (error: any) { - const metaTO = this.getTags(destType, 'unknown', 'unknown', tags.FEATURES.USER_DELETION); return DestinationPostTransformationService.handleUserDeletionFailureEvents( error, metaTO, diff --git a/src/services/destination/postTransformation.ts b/src/services/destination/postTransformation.ts index 161547683b..40cee61e66 100644 --- a/src/services/destination/postTransformation.ts +++ b/src/services/destination/postTransformation.ts @@ -1,24 +1,30 @@ /* eslint-disable no-param-reassign */ +import { PlatformError } from '@rudderstack/integrations-lib'; import cloneDeep from 'lodash/cloneDeep'; -import isObject from 'lodash/isObject'; import isEmpty from 'lodash/isEmpty'; -import { PlatformError } from '@rudderstack/integrations-lib'; +import isObject from 'lodash/isObject'; import { + DeliveryJobState, + DeliveryV0Response, + DeliveryV1Response, + MetaTransferObject, + ProcessorTransformationOutput, ProcessorTransformationRequest, ProcessorTransformationResponse, RouterTransformationResponse, - ProcessorTransformationOutput, - DeliveryV0Response, - MetaTransferObject, UserDeletionResponse, - DeliveryV1Response, - DeliveryJobState, } from '../../types/index'; -import { generateErrorObject } from '../../v0/util'; -import { ErrorReportingService } from '../errorReporting'; -import tags from '../../v0/util/tags'; import stats from '../../util/stats'; import { FixMe } from '../../util/types'; +import { generateErrorObject } from '../../v0/util'; +import tags from '../../v0/util/tags'; +import { ErrorReportingService } from '../errorReporting'; +import { MiscService } from '../misc'; + +const defaultErrorMessages = { + router: '[Router Transform] Error occurred while processing the payload.', + delivery: '[Delivery] Error occured while processing payload', +} as const; export class DestinationPostTransformationService { public static handleProcessorTransformSucessEvents( @@ -62,6 +68,10 @@ export class DestinationPostTransformationService { error: errObj.message || '[Processor Transform] Error occurred while processing the payload.', statTags: errObj.statTags, } as ProcessorTransformationResponse; + MiscService.logError( + errObj.message || '[Processor Transform] Error occurred while processing the payload.', + metaTo.errorDetails, + ); ErrorReportingService.reportError(error, metaTo.errorContext, resp); return resp; } @@ -99,6 +109,7 @@ export class DestinationPostTransformationService { ...resp.statTags, ...metaTo.errorDetails, }; + MiscService.logError(resp.error || defaultErrorMessages.router, metaTo.errorDetails); stats.increment('event_transform_failure', metaTo.errorDetails); } else { stats.increment('event_transform_success', { @@ -124,9 +135,10 @@ export class DestinationPostTransformationService { metadata: metaTo.metadatas, batched: false, statusCode: errObj.status, - error: errObj.message || '[Router Transform] Error occurred while processing the payload.', + error: errObj.message || defaultErrorMessages.router, statTags: errObj.statTags, } as RouterTransformationResponse; + MiscService.logError(errObj.message || defaultErrorMessages.router, metaTo.errorDetails); ErrorReportingService.reportError(error, metaTo.errorContext, resp); stats.increment('event_transform_failure', metaTo.errorDetails); return resp; @@ -141,9 +153,10 @@ export class DestinationPostTransformationService { metadata: metaTo.metadatas, batched: false, statusCode: 500, // for batch we should consider code error hence keeping retryable - error: errObj.message || '[Batch Transform] Error occurred while processing payload.', + error: errObj.message || defaultErrorMessages.delivery, statTags: errObj.statTags, } as RouterTransformationResponse; + MiscService.logError(error as string, metaTo.errorDetails); ErrorReportingService.reportError(error, metaTo.errorContext, resp); return resp; } @@ -174,6 +187,10 @@ export class DestinationPostTransformationService { const errObj = generateErrorObject(error, metaTo.errorDetails, false); const metadataArray = metaTo.metadatas; if (!Array.isArray(metadataArray)) { + MiscService.logError( + 'Proxy v1 endpoint error : metadataArray is not an array', + metaTo.errorDetails, + ); // Panic throw new PlatformError('Proxy v1 endpoint error : metadataArray is not an array'); } @@ -182,7 +199,7 @@ export class DestinationPostTransformationService { error: JSON.stringify(error.destinationResponse?.response) || errObj.message || - '[Delivery] Error occured while processing payload', + defaultErrorMessages.delivery, statusCode: errObj.status, metadata, } as DeliveryJobState; @@ -198,7 +215,7 @@ export class DestinationPostTransformationService { authErrorCategory: errObj.authErrorCategory, }), } as DeliveryV1Response; - + MiscService.logError(errObj.message, metaTo.errorDetails); ErrorReportingService.reportError(error, metaTo.errorContext, resp); return resp; } @@ -216,6 +233,7 @@ export class DestinationPostTransformationService { authErrorCategory: errObj.authErrorCategory, }), } as UserDeletionResponse; + MiscService.logError(errObj.message, metaTo.errorDetails); ErrorReportingService.reportError(error, metaTo.errorContext, resp); return resp; } diff --git a/src/services/misc.ts b/src/services/misc.ts index e0953d08bf..3df1196c1d 100644 --- a/src/services/misc.ts +++ b/src/services/misc.ts @@ -1,10 +1,11 @@ /* eslint-disable global-require, import/no-dynamic-require */ +import { LoggableExtraData, structuredLogger as logger } from '@rudderstack/integrations-lib'; import fs from 'fs'; -import path from 'path'; import { Context } from 'koa'; +import path from 'path'; import { DestHandlerMap } from '../constants/destinationCanonicalNames'; -import { Metadata } from '../types'; import { getCPUProfile, getHeapProfile } from '../middleware'; +import { ErrorDetailer, Metadata } from '../types'; export class MiscService { public static getDestHandler(dest: string, version: string) { @@ -74,4 +75,21 @@ export class MiscService { public static async getHeapProfile() { return getHeapProfile(); } + + public static getLoggableData(errorDetailer: ErrorDetailer): Partial { + return { + ...(errorDetailer?.destinationId && { destinationId: errorDetailer.destinationId }), + ...(errorDetailer?.sourceId && { sourceId: errorDetailer.sourceId }), + ...(errorDetailer?.workspaceId && { workspaceId: errorDetailer.workspaceId }), + ...(errorDetailer?.destType && { destType: errorDetailer.destType }), + ...(errorDetailer?.module && { module: errorDetailer.module }), + ...(errorDetailer?.implementation && { implementation: errorDetailer.implementation }), + ...(errorDetailer?.feature && { feature: errorDetailer.feature }), + }; + } + + public static logError(message: string, errorDetailer: ErrorDetailer) { + const loggableExtraData: Partial = this.getLoggableData(errorDetailer); + logger.errorw(message || '', loggableExtraData); + } } diff --git a/src/services/source/__tests__/nativeIntegration.test.ts b/src/services/source/__tests__/nativeIntegration.test.ts index bb40438811..77e355fd1a 100644 --- a/src/services/source/__tests__/nativeIntegration.test.ts +++ b/src/services/source/__tests__/nativeIntegration.test.ts @@ -1,8 +1,9 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; +import { FetchHandler } from '../../../helpers/fetchHandlers'; +import { RudderMessage, SourceTransformationResponse } from '../../../types/index'; +import stats from '../../../util/stats'; import { NativeIntegrationSourceService } from '../nativeIntegration'; import { SourcePostTransformationService } from '../postTransformation'; -import { SourceTransformationResponse, RudderMessage } from '../../../types/index'; -import stats from '../../../util/stats'; -import { FetchHandler } from '../../../helpers/fetchHandlers'; afterEach(() => { jest.clearAllMocks(); @@ -43,7 +44,13 @@ describe('NativeIntegration Source Service', () => { }); const service = new NativeIntegrationSourceService(); - const resp = await service.sourceTransformRoutine(events, sourceType, version, requestMetadata); + const resp = await service.sourceTransformRoutine( + events, + sourceType, + version, + requestMetadata, + logger, + ); expect(resp).toEqual(tresponse); @@ -80,7 +87,13 @@ describe('NativeIntegration Source Service', () => { jest.spyOn(stats, 'increment').mockImplementation(() => {}); const service = new NativeIntegrationSourceService(); - const resp = await service.sourceTransformRoutine(events, sourceType, version, requestMetadata); + const resp = await service.sourceTransformRoutine( + events, + sourceType, + version, + requestMetadata, + logger, + ); expect(resp).toEqual(tresponse); diff --git a/src/services/source/nativeIntegration.ts b/src/services/source/nativeIntegration.ts index 6eaef2f835..2ecfc30066 100644 --- a/src/services/source/nativeIntegration.ts +++ b/src/services/source/nativeIntegration.ts @@ -1,3 +1,4 @@ +import { FetchHandler } from '../../helpers/fetchHandlers'; import { SourceService } from '../../interfaces/SourceService'; import { ErrorDetailer, @@ -5,11 +6,11 @@ import { RudderMessage, SourceTransformationResponse, } from '../../types/index'; +import stats from '../../util/stats'; import { FixMe } from '../../util/types'; -import { SourcePostTransformationService } from './postTransformation'; -import { FetchHandler } from '../../helpers/fetchHandlers'; import tags from '../../v0/util/tags'; -import stats from '../../util/stats'; +import { MiscService } from '../misc'; +import { SourcePostTransformationService } from './postTransformation'; export class NativeIntegrationSourceService implements SourceService { public getTags(): MetaTransferObject { @@ -31,20 +32,23 @@ export class NativeIntegrationSourceService implements SourceService { version: string, // eslint-disable-next-line @typescript-eslint/no-unused-vars _requestMetadata: NonNullable, + logger: FixMe, ): Promise { const sourceHandler = FetchHandler.getSourceHandler(sourceType, version); + const metaTO = this.getTags(); + const loggerWithCtx = logger.child({ ...MiscService.getLoggableData(metaTO.errorDetails) }); const respList: SourceTransformationResponse[] = await Promise.all( sourceEvents.map(async (sourceEvent) => { try { const respEvents: RudderMessage | RudderMessage[] | SourceTransformationResponse = - await sourceHandler.process(sourceEvent); + await sourceHandler.process(sourceEvent, loggerWithCtx); return SourcePostTransformationService.handleSuccessEventsSource(respEvents); } catch (error: FixMe) { - const metaTO = this.getTags(); stats.increment('source_transform_errors', { source: sourceType, version, }); + logger.debug('Error during source Transform', error); return SourcePostTransformationService.handleFailureEventsSource(error, metaTO); } }), diff --git a/src/util/eventValidation.js b/src/util/eventValidation.js index 68d895dcc5..9f3ecd859d 100644 --- a/src/util/eventValidation.js +++ b/src/util/eventValidation.js @@ -126,7 +126,7 @@ async function validate(event) { trackingPlanId, trackingPlanVersion, event.message.type, - event.message.event, + event.message.type === 'track' ? event.message.event : '', workspaceId, ); diff --git a/src/util/openfaas/faasApi.js b/src/util/openfaas/faasApi.js index 4db5e2a81c..f8f830f6e4 100644 --- a/src/util/openfaas/faasApi.js +++ b/src/util/openfaas/faasApi.js @@ -2,6 +2,13 @@ const axios = require('axios'); const { RespStatusError, RetryRequestError } = require('../utils'); const OPENFAAS_GATEWAY_URL = process.env.OPENFAAS_GATEWAY_URL || 'http://localhost:8080'; +const OPENFAAS_GATEWAY_USERNAME = process.env.OPENFAAS_GATEWAY_USERNAME || ''; +const OPENFAAS_GATEWAY_PASSWORD = process.env.OPENFAAS_GATEWAY_PASSWORD || ''; + +const basicAuth = { + username: OPENFAAS_GATEWAY_USERNAME, + password: OPENFAAS_GATEWAY_PASSWORD, +}; const parseAxiosError = (error) => { if (error.response) { @@ -21,7 +28,7 @@ const deleteFunction = async (functionName) => new Promise((resolve, reject) => { const url = `${OPENFAAS_GATEWAY_URL}/system/functions`; axios - .delete(url, { data: { functionName } }) + .delete(url, { data: { functionName }, auth: basicAuth }) .then(() => resolve()) .catch((err) => reject(parseAxiosError(err))); }); @@ -30,7 +37,7 @@ const getFunction = async (functionName) => new Promise((resolve, reject) => { const url = `${OPENFAAS_GATEWAY_URL}/system/function/${functionName}`; axios - .get(url) + .get(url, { auth: basicAuth }) .then((resp) => resolve(resp.data)) .catch((err) => reject(parseAxiosError(err))); }); @@ -39,7 +46,7 @@ const getFunctionList = async () => new Promise((resolve, reject) => { const url = `${OPENFAAS_GATEWAY_URL}/system/functions`; axios - .get(url) + .get(url, { auth: basicAuth }) .then((resp) => resolve(resp.data)) .catch((err) => reject(parseAxiosError(err))); }); @@ -48,29 +55,36 @@ const invokeFunction = async (functionName, payload) => new Promise((resolve, reject) => { const url = `${OPENFAAS_GATEWAY_URL}/function/${functionName}`; axios - .post(url, payload) + .post(url, payload, { auth: basicAuth }) .then((resp) => resolve(resp.data)) .catch((err) => reject(parseAxiosError(err))); }); -const checkFunctionHealth = async (functionName) => - new Promise((resolve, reject) => { +const checkFunctionHealth = async (functionName) => { + return new Promise((resolve, reject) => { const url = `${OPENFAAS_GATEWAY_URL}/function/${functionName}`; axios - .get(url, { - headers: { 'X-REQUEST-TYPE': 'HEALTH-CHECK' }, - }) + .get( + url, + { + headers: { 'X-REQUEST-TYPE': 'HEALTH-CHECK' }, + }, + { auth: basicAuth }, + ) .then((resp) => resolve(resp)) .catch((err) => reject(parseAxiosError(err))); }); +}; const deployFunction = async (payload) => new Promise((resolve, reject) => { const url = `${OPENFAAS_GATEWAY_URL}/system/functions`; axios - .post(url, payload) + .post(url, payload, { auth: basicAuth }) .then((resp) => resolve(resp.data)) - .catch((err) => reject(parseAxiosError(err))); + .catch((err) => { + reject(parseAxiosError(err)); + }); }); module.exports = { diff --git a/src/util/openfaas/index.js b/src/util/openfaas/index.js index 878fa706d9..7a1fce3cfa 100644 --- a/src/util/openfaas/index.js +++ b/src/util/openfaas/index.js @@ -11,6 +11,11 @@ const stats = require('../stats'); const { getMetadata, getTransformationMetadata } = require('../../v0/util'); const { HTTP_STATUS_CODES } = require('../../v0/util/constant'); +const FAAS_SCALE_TYPE = process.env.FAAS_SCALE_TYPE || 'capacity'; +const FAAS_SCALE_TARGET = process.env.FAAS_SCALE_TARGET || '4'; +const FAAS_SCALE_TARGET_PROPORTION = process.env.FAAS_SCALE_TARGET_PROPORTION || '0.70'; +const FAAS_SCALE_ZERO = process.env.FAAS_SCALE_ZERO || 'false'; +const FAAS_SCALE_ZERO_DURATION = process.env.FAAS_SCALE_ZERO_DURATION || '15m'; const FAAS_BASE_IMG = process.env.FAAS_BASE_IMG || 'rudderlabs/openfaas-flask:main'; const FAAS_MAX_PODS_IN_TEXT = process.env.FAAS_MAX_PODS_IN_TEXT || '40'; const FAAS_MIN_PODS_IN_TEXT = process.env.FAAS_MIN_PODS_IN_TEXT || '1'; @@ -27,6 +32,7 @@ const FAAS_AST_VID = 'ast'; const FAAS_AST_FN_NAME = 'fn-ast'; const CUSTOM_NETWORK_POLICY_WORKSPACE_IDS = process.env.CUSTOM_NETWORK_POLICY_WORKSPACE_IDS || ''; const customNetworkPolicyWorkspaceIds = CUSTOM_NETWORK_POLICY_WORKSPACE_IDS.split(','); +const CUSTOMER_TIER = process.env.CUSTOMER_TIER || 'shared'; // Initialise node cache const functionListCache = new NodeCache(); @@ -124,7 +130,7 @@ const deployFaasFunction = async ( trMetadata = {}, ) => { try { - logger.debug('[Faas] Deploying a faas function'); + logger.debug(`[Faas] Deploying a faas function: ${functionName}`); let envProcess = 'python index.py'; const lvidsString = libraryVersionIDs.join(','); @@ -149,8 +155,17 @@ const deployFaasFunction = async ( 'parent-component': 'openfaas', 'com.openfaas.scale.max': FAAS_MAX_PODS_IN_TEXT, 'com.openfaas.scale.min': FAAS_MIN_PODS_IN_TEXT, + 'com.openfaas.scale.zero': FAAS_SCALE_ZERO, + 'com.openfaas.scale.zero-duration': FAAS_SCALE_ZERO_DURATION, + 'com.openfaas.scale.target': FAAS_SCALE_TARGET, + 'com.openfaas.scale.target-proportion': FAAS_SCALE_TARGET_PROPORTION, + 'com.openfaas.scale.type': FAAS_SCALE_TYPE, transformationId: trMetadata.transformationId, workspaceId: trMetadata.workspaceId, + team: 'data-management', + service: 'openfaas-fn', + customer: 'shared', + 'customer-tier': CUSTOMER_TIER, }; if ( trMetadata.workspaceId && diff --git a/src/util/prometheus.js b/src/util/prometheus.js index 09d0359b5d..882dff9e75 100644 --- a/src/util/prometheus.js +++ b/src/util/prometheus.js @@ -611,7 +611,7 @@ class Prometheus { name: 'http_request_duration', help: 'Incoming HTTP requests duration in seconds', type: 'histogram', - labelNames: ['method', 'route', 'code'], + labelNames: ['method', 'route', 'code', 'destType'], }, { name: 'tp_batch_size', diff --git a/src/util/redis/redisConnector.test.js b/src/util/redis/redisConnector.test.js index e0491132ff..7cf2ccbbcf 100644 --- a/src/util/redis/redisConnector.test.js +++ b/src/util/redis/redisConnector.test.js @@ -2,6 +2,7 @@ const fs = require('fs'); const path = require('path'); const version = 'v0'; const { RedisDB } = require('./redisConnector'); +const { structuredLogger: logger } = require('@rudderstack/integrations-lib'); jest.mock('ioredis', () => require('../../../test/__mocks__/redis')); const sourcesList = ['shopify']; process.env.USE_REDIS_DB = 'true'; @@ -54,7 +55,7 @@ describe(`Source Tests`, () => { data.forEach((dataPoint, index) => { it(`${index}. ${source} - ${dataPoint.description}`, async () => { try { - const output = await transformer.process(dataPoint.input); + const output = await transformer.process(dataPoint.input, logger); expect(output).toEqual(dataPoint.output); } catch (error) { expect(error.message).toEqual(dataPoint.output.error); diff --git a/src/util/redis/testData/shopify_source.json b/src/util/redis/testData/shopify_source.json index 04b80b8fc9..2120475baf 100644 --- a/src/util/redis/testData/shopify_source.json +++ b/src/util/redis/testData/shopify_source.json @@ -65,11 +65,8 @@ } }, "output": { - "outputToSource": { - "body": "T0s=", - "contentType": "text/plain" - }, - "statusCode": 200 + "error": "Error: Error setting value in Redis due Error: Connection is Closed", + "statusCode": 500 } }, { diff --git a/src/util/trackingPlan.js b/src/util/trackingPlan.js index a77265a5b8..ebfbc6049f 100644 --- a/src/util/trackingPlan.js +++ b/src/util/trackingPlan.js @@ -55,6 +55,11 @@ async function getEventSchema(tpId, tpVersion, eventType, eventName, workspaceId let eventSchema; const tp = await getTrackingPlan(tpId, tpVersion, workspaceId); + if (Object.hasOwn(tp, 'events')) { + const ev = tp.events.find((e) => e.name === eventName && e.eventType === eventType); + return ev?.rules; + } + if (eventType !== 'track') { if (Object.prototype.hasOwnProperty.call(tp.rules, eventType)) { eventSchema = tp.rules[eventType]; diff --git a/src/util/utils.js b/src/util/utils.js index 0ba6008368..d74603dd7a 100644 --- a/src/util/utils.js +++ b/src/util/utils.js @@ -22,6 +22,7 @@ const staticLookup = (transformerVersionId) => async (hostname, _, cb) => { try { ips = await resolver.resolve4(hostname); } catch (error) { + logger.error(`DNS Error Code: ${error.code} | Message : ${error.message}`); stats.timing('fetch_dns_resolve_time', resolveStartTime, { transformerVersionId, error: 'true', diff --git a/src/v0/destinations/active_campaign/data/ACIdentify.json b/src/v0/destinations/active_campaign/data/ACIdentify.json index 6cee9e72c6..134955d52e 100644 --- a/src/v0/destinations/active_campaign/data/ACIdentify.json +++ b/src/v0/destinations/active_campaign/data/ACIdentify.json @@ -1,7 +1,7 @@ [ { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": true }, diff --git a/src/v0/destinations/awin/transform.js b/src/v0/destinations/awin/transform.js index 49a115c1ff..0d7fd95c33 100644 --- a/src/v0/destinations/awin/transform.js +++ b/src/v0/destinations/awin/transform.js @@ -2,10 +2,12 @@ const { InstrumentationError, ConfigurationError } = require('@rudderstack/integ const { BASE_URL, ConfigCategory, mappingConfig } = require('./config'); const { defaultRequestConfig, constructPayload, simpleProcessRouterDest } = require('../../util'); -const { getParams } = require('./utils'); +const { getParams, trackProduct } = require('./utils'); const responseBuilder = (message, { Config }) => { const { advertiserId, eventsToTrack } = Config; + const { event, properties } = message; + let finalParams = {}; const payload = constructPayload(message, mappingConfig[ConfigCategory.TRACK.name]); @@ -17,8 +19,14 @@ const responseBuilder = (message, { Config }) => { }); // if the event is present in eventsList - if (eventsList.includes(message.event)) { + if (eventsList.includes(event)) { params = getParams(payload.params, advertiserId); + const productTrackObject = trackProduct(properties, advertiserId, params.parts); + + finalParams = { + ...params, + ...productTrackObject, + }; } else { throw new InstrumentationError( "Event is not present in 'Events to Track' list. Aborting message.", @@ -27,7 +35,7 @@ const responseBuilder = (message, { Config }) => { } } const response = defaultRequestConfig(); - response.params = params; + response.params = finalParams; response.endpoint = BASE_URL; return response; diff --git a/src/v0/destinations/awin/utils.js b/src/v0/destinations/awin/utils.js index 1616227c55..f0daea9b99 100644 --- a/src/v0/destinations/awin/utils.js +++ b/src/v0/destinations/awin/utils.js @@ -1,3 +1,5 @@ +const lodash = require('lodash'); + /** * Returns final params * @param {*} params @@ -24,6 +26,59 @@ const getParams = (parameters, advertiserId) => { return params; }; +const areAllValuesDefined = (obj) => + lodash.every(lodash.values(obj), (value) => !lodash.isUndefined(value)); + +const buildProductPayloadString = (payload) => { + // URL-encode each value, and join back with the same key. + const encodedPayload = Object.entries(payload).reduce((acc, [key, value]) => { + // Encode each value. Assuming that all values are either strings or can be + // safely converted to strings. + acc[key] = encodeURIComponent(value); + return acc; + }, {}); + + return `AW:P|${encodedPayload.advertiserId}|${encodedPayload.orderReference}|${encodedPayload.productId}|${encodedPayload.productName}|${encodedPayload.productItemPrice}|${encodedPayload.productQuantity}|${encodedPayload.productSku}|${encodedPayload.commissionGroupCode}|${encodedPayload.productCategory}`; +}; + +// ref: https://wiki.awin.com/index.php/Advertiser_Tracking_Guide/Product_Level_Tracking#PLT_Via_Conversion_Pixel +const trackProduct = (properties, advertiserId, commissionParts) => { + const transformedProductInfoObj = {}; + if ( + properties?.products && + Array.isArray(properties?.products) && + properties.products.length > 0 + ) { + const productsArray = properties.products; + let productIndex = 0; + productsArray.forEach((product) => { + const productPayloadNew = { + advertiserId, + orderReference: + properties.order_id || + properties.orderId || + properties.orderReference || + properties.order_reference, + productId: product.product_id || product.productId, + productName: product.name, + productItemPrice: product.price, + productQuantity: product.quantity, + productSku: product.sku || '', + commissionGroupCode: commissionParts || 'DEFAULT', + productCategory: product.category || '', + }; + if (areAllValuesDefined(productPayloadNew)) { + transformedProductInfoObj[`bd[${productIndex}]`] = + buildProductPayloadString(productPayloadNew); + productIndex += 1; + } + }); + } + return transformedProductInfoObj; +}; + module.exports = { getParams, + trackProduct, + buildProductPayloadString, }; diff --git a/src/v0/destinations/awin/utils.test.js b/src/v0/destinations/awin/utils.test.js new file mode 100644 index 0000000000..e60c07e96c --- /dev/null +++ b/src/v0/destinations/awin/utils.test.js @@ -0,0 +1,165 @@ +const { buildProductPayloadString, trackProduct } = require('./utils'); + +describe('buildProductPayloadString', () => { + // Should correctly build the payload string with all fields provided + it('should correctly build the payload string with all fields provided', () => { + const payload = { + advertiserId: '123', + orderReference: 'order123', + productId: 'prod123', + productName: 'Product 1', + productItemPrice: '10.99', + productQuantity: '2', + productSku: 'sku123', + commissionGroupCode: 'DEFAULT', + productCategory: 'Category 1', + }; + + const expected = 'AW:P|123|order123|prod123|Product%201|10.99|2|sku123|DEFAULT|Category%201'; + const result = buildProductPayloadString(payload); + + expect(result).toBe(expected); + }); + + // Should correctly handle extremely long string values for all fields + it('should correctly handle extremely long string values for all fields', () => { + const payload = { + advertiserId: '123', + orderReference: 'order123', + productId: 'prod123', + productName: 'Product 1'.repeat(100000), + productItemPrice: '10.99'.repeat(100000), + productQuantity: '2'.repeat(100000), + productSku: 'sku123'.repeat(100000), + commissionGroupCode: 'DEFAULT', + productCategory: 'Category 1'.repeat(100000), + }; + + const expected = `AW:P|123|order123|prod123|${encodeURIComponent('Product 1'.repeat(100000))}|${encodeURIComponent('10.99'.repeat(100000))}|${encodeURIComponent('2'.repeat(100000))}|${encodeURIComponent('sku123'.repeat(100000))}|DEFAULT|${encodeURIComponent('Category 1'.repeat(100000))}`; + const result = buildProductPayloadString(payload); + + expect(result).toBe(expected); + }); +}); + +describe('trackProduct', () => { + // Given a valid 'properties' object with a non-empty 'products' array, it should transform each product into a valid payload string and return an object with the transformed products. + it("should transform each product into a valid payload string and return an object with the transformed products when given a valid 'properties' object with a non-empty 'products' array", () => { + // Arrange + const properties = { + products: [ + { + product_id: '123', + name: 'Product 1', + price: 10, + quantity: 1, + sku: 'SKU123', + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: 'Category 2', + }, + ], + order_id: 'order123', + }; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({ + 'bd[0]': 'AW:P|advertiser123|order123|123|Product%201|10|1|SKU123|COMMISSION|Category%201', + 'bd[1]': 'AW:P|advertiser123|order123|456|Product%202|20|2|SKU456|COMMISSION|Category%202', + }); + }); + + // Given an invalid 'properties' object, it should return an empty object. + it("should return an empty object when given an invalid 'properties' object", () => { + // Arrange + const properties = {}; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({}); + }); + + it('should ignore the product which has missing properties', () => { + // Arrange + const properties = { + products: [ + { + price: 10, + quantity: 1, + sku: 'SKU123', + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: 'Category 2', + }, + ], + order_id: 'order123', + }; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({ + 'bd[0]': 'AW:P|advertiser123|order123|456|Product%202|20|2|SKU456|COMMISSION|Category%202', + }); + }); + + it('category and sku if undefined we put blank', () => { + // Arrange + const properties = { + products: [ + { + product_id: '123', + name: 'Product 1', + price: 10, + quantity: 1, + sku: undefined, + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: undefined, + }, + ], + order_id: 'order123', + }; + const advertiserId = 'advertiser123'; + const commissionParts = 'COMMISSION'; + + // Act + const result = trackProduct(properties, advertiserId, commissionParts); + + // Assert + expect(result).toEqual({ + 'bd[0]': 'AW:P|advertiser123|order123|123|Product%201|10|1||COMMISSION|Category%201', + 'bd[1]': 'AW:P|advertiser123|order123|456|Product%202|20|2|SKU456|COMMISSION|', + }); + }); +}); diff --git a/src/v0/destinations/blueshift/data/blueshiftGroupConfig.json b/src/v0/destinations/blueshift/data/blueshiftGroupConfig.json index 41e846c509..e246a059d9 100644 --- a/src/v0/destinations/blueshift/data/blueshiftGroupConfig.json +++ b/src/v0/destinations/blueshift/data/blueshiftGroupConfig.json @@ -13,7 +13,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/blueshift/data/blueshiftIdentifyConfig.json b/src/v0/destinations/blueshift/data/blueshiftIdentifyConfig.json index cd099b2cc3..506c313aa1 100644 --- a/src/v0/destinations/blueshift/data/blueshiftIdentifyConfig.json +++ b/src/v0/destinations/blueshift/data/blueshiftIdentifyConfig.json @@ -1,7 +1,7 @@ [ { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": true }, diff --git a/src/v0/destinations/blueshift/data/blueshiftTrackConfig.json b/src/v0/destinations/blueshift/data/blueshiftTrackConfig.json index 5fb1be691a..9841450754 100644 --- a/src/v0/destinations/blueshift/data/blueshiftTrackConfig.json +++ b/src/v0/destinations/blueshift/data/blueshiftTrackConfig.json @@ -7,7 +7,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/campaign_manager/transform.js b/src/v0/destinations/campaign_manager/transform.js index 14bc6d2c19..403a79a971 100644 --- a/src/v0/destinations/campaign_manager/transform.js +++ b/src/v0/destinations/campaign_manager/transform.js @@ -243,7 +243,8 @@ const batchEvents = (eventChunksArray) => { return batchedResponseList; }; -const processRouterDest = async (inputs, reqMetadata) => { +const processRouterDest = async (inputs, reqMetadata, logger) => { + logger.debug(`Transformation router request received with size ${inputs.length}`); const batchErrorRespList = []; const eventChunksArray = []; const { destination } = inputs[0]; diff --git a/src/v0/destinations/custify/data/CUSTIFYIdentifyConfig.json b/src/v0/destinations/custify/data/CUSTIFYIdentifyConfig.json index 4a7a785e1d..00a8ea0584 100644 --- a/src/v0/destinations/custify/data/CUSTIFYIdentifyConfig.json +++ b/src/v0/destinations/custify/data/CUSTIFYIdentifyConfig.json @@ -7,7 +7,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/custify/data/CUSTIFYTrackConfig.json b/src/v0/destinations/custify/data/CUSTIFYTrackConfig.json index f68e9f18ac..1751c84230 100644 --- a/src/v0/destinations/custify/data/CUSTIFYTrackConfig.json +++ b/src/v0/destinations/custify/data/CUSTIFYTrackConfig.json @@ -7,7 +7,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/delighted/util.js b/src/v0/destinations/delighted/util.js index c690bf5f5c..53f416b48d 100644 --- a/src/v0/destinations/delighted/util.js +++ b/src/v0/destinations/delighted/util.js @@ -1,14 +1,10 @@ -const { - NetworkInstrumentationError, - InstrumentationError, - NetworkError, -} = require('@rudderstack/integrations-lib'); -const myAxios = require('../../../util/myAxios'); +const { InstrumentationError, NetworkError } = require('@rudderstack/integrations-lib'); const { getDynamicErrorType } = require('../../../adapters/utils/networkUtils'); const { getValueFromMessage } = require('../../util'); const { ENDPOINT } = require('./config'); const tags = require('../../util/tags'); const { JSON_MIME_TYPE } = require('../../util/constant'); +const { handleHttpRequest } = require('../../../adapters/network'); const isValidEmail = (email) => { const re = @@ -41,6 +37,30 @@ const isValidUserIdOrError = (channel, userId) => { }; }; +/** + * Returns final status + * @param {*} status + * @returns + */ +const getErrorStatus = (status) => { + let errStatus; + switch (status) { + case 422: + case 401: + case 406: + case 403: + errStatus = 400; + break; + case 500: + case 503: + errStatus = 500; + break; + default: + errStatus = status; + } + return errStatus; +}; + const userValidity = async (channel, Config, userId) => { const paramsdata = {}; if (channel === 'email') { @@ -50,53 +70,38 @@ const userValidity = async (channel, Config, userId) => { } const basicAuth = Buffer.from(Config.apiKey).toString('base64'); - let response; - try { - response = await myAxios.get( - `${ENDPOINT}`, - { - headers: { - Authorization: `Basic ${basicAuth}`, - 'Content-Type': JSON_MIME_TYPE, - }, - params: paramsdata, + const { processedResponse } = await handleHttpRequest( + 'get', + `${ENDPOINT}`, + { + headers: { + Authorization: `Basic ${basicAuth}`, + 'Content-Type': JSON_MIME_TYPE, }, - { - destType: 'delighted', - feature: 'transformation', - requestMethod: 'GET', - endpointPath: '/people.json', - module: 'router', - }, - ); - if (response && response.data && response.status === 200 && Array.isArray(response.data)) { - return response.data.length > 0; - } - throw new NetworkInstrumentationError('Invalid response'); - } catch (error) { - let errMsg = ''; - let errStatus = 400; - if (error.response && error.response.data) { - errMsg = JSON.stringify(error.response.data); - switch (error.response.status) { - case 422: - case 401: - case 406: - case 403: - errStatus = 400; - break; - case 500: - case 503: - errStatus = 500; - break; - default: - errStatus = 400; - } - } - throw new NetworkError(`Error occurred while checking user : ${errMsg}`, errStatus, { - [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(errStatus), - }); + params: paramsdata, + }, + { + destType: 'delighted', + feature: 'transformation', + requestMethod: 'GET', + endpointPath: '/people.json', + module: 'router', + }, + ); + + if (processedResponse.status === 200 && Array.isArray(processedResponse?.response)) { + return processedResponse.response.length > 0; } + + const errStatus = getErrorStatus(processedResponse.status); + throw new NetworkError( + `Error occurred while checking user: ${JSON.stringify(processedResponse?.response || 'Invalid response')}`, + errStatus, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(errStatus), + }, + processedResponse, + ); }; const eventValidity = (Config, message) => { const event = getValueFromMessage(message, 'event'); diff --git a/src/v0/destinations/facebook_offline_conversions/data/FbOfflineConversionsTrackConfig.json b/src/v0/destinations/facebook_offline_conversions/data/FbOfflineConversionsTrackConfig.json index fc5f6c48fa..3f2fa1875f 100644 --- a/src/v0/destinations/facebook_offline_conversions/data/FbOfflineConversionsTrackConfig.json +++ b/src/v0/destinations/facebook_offline_conversions/data/FbOfflineConversionsTrackConfig.json @@ -39,7 +39,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/freshmarketer/data/FRESHMARKETERIdentifyConfig.json b/src/v0/destinations/freshmarketer/data/FRESHMARKETERIdentifyConfig.json index da1a8bccbe..d8abb484b2 100644 --- a/src/v0/destinations/freshmarketer/data/FRESHMARKETERIdentifyConfig.json +++ b/src/v0/destinations/freshmarketer/data/FRESHMARKETERIdentifyConfig.json @@ -1,7 +1,7 @@ [ { "destKey": "emails", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": true }, diff --git a/src/v0/destinations/freshsales/data/identifyConfig.json b/src/v0/destinations/freshsales/data/identifyConfig.json index 9be9d9d855..94a15fd43d 100644 --- a/src/v0/destinations/freshsales/data/identifyConfig.json +++ b/src/v0/destinations/freshsales/data/identifyConfig.json @@ -1,7 +1,7 @@ [ { "destKey": "emails", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": true }, diff --git a/src/v0/destinations/ga4/transform.js b/src/v0/destinations/ga4/transform.js index d8fc531e92..5280a46dab 100644 --- a/src/v0/destinations/ga4/transform.js +++ b/src/v0/destinations/ga4/transform.js @@ -27,6 +27,7 @@ const { const { getItemsArray, validateEventName, + prepareUserConsents, removeInvalidParams, isReservedEventName, getGA4ExclusionList, @@ -238,6 +239,12 @@ const responseBuilder = (message, { Config }) => { rawPayload.user_properties = userProperties; } + // Prepare GA4 consents + const consents = prepareUserConsents(message); + if (!isEmptyObject(consents)) { + rawPayload.consent = consents; + } + payload = removeUndefinedAndNullValues(payload); rawPayload = { ...rawPayload, events: [payload] }; diff --git a/src/v0/destinations/ga4/utils.js b/src/v0/destinations/ga4/utils.js index e4db494727..ce8afda560 100644 --- a/src/v0/destinations/ga4/utils.js +++ b/src/v0/destinations/ga4/utils.js @@ -7,8 +7,10 @@ const { isEmptyObject, extractCustomFields, isDefinedAndNotNull, + getIntegrationsObj, } = require('../../util'); const { mappingConfig, ConfigCategory } = require('./config'); +const { finaliseAnalyticsConsents } = require('../../util/googleUtils'); /** * Reserved event names cannot be used @@ -432,11 +434,30 @@ const prepareUserProperties = (message, piiPropertiesToIgnore = []) => { return validatedUserProperties; }; +/** + * Returns user consents + * Ref : https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference?client_type=gtag#payload_consent + * @param {*} message + * @returns + */ +const prepareUserConsents = (message) => { + const integrationObj = getIntegrationsObj(message, 'ga4') || {}; + const eventLevelConsentsData = integrationObj?.consents || {}; + const consentConfigMap = { + analyticsPersonalizationConsent: 'ad_user_data', + analyticsUserDataConsent: 'ad_personalization', + }; + + const consents = finaliseAnalyticsConsents(consentConfigMap, eventLevelConsentsData); + return consents; +}; + module.exports = { getItem, getItemList, getItemsArray, validateEventName, + prepareUserConsents, removeInvalidParams, isReservedEventName, getGA4ExclusionList, diff --git a/src/v0/destinations/ga4/utils.test.js b/src/v0/destinations/ga4/utils.test.js index 18b3ab5766..501778910f 100644 --- a/src/v0/destinations/ga4/utils.test.js +++ b/src/v0/destinations/ga4/utils.test.js @@ -1,4 +1,9 @@ -const { validateEventName, prepareUserProperties, removeInvalidParams } = require('./utils'); +const { + validateEventName, + removeInvalidParams, + prepareUserConsents, + prepareUserProperties, +} = require('./utils'); const userPropertyData = [ { @@ -447,4 +452,84 @@ describe('Google Analytics 4 utils test', () => { expect(result).toEqual(expected); }); }); + + describe('prepareUserConsents function tests', () => { + it('Should return an empty object when no consents are given', () => { + const message = {}; + const result = prepareUserConsents(message); + expect(result).toEqual({}); + }); + + it('Should return an empty object when no consents are given', () => { + const message = { + integrations: { + GA4: {}, + }, + }; + const result = prepareUserConsents(message); + expect(result).toEqual({}); + }); + + it('Should return an empty object when no consents are given', () => { + const message = { + integrations: { + GA4: { + consents: {}, + }, + }, + }; + const result = prepareUserConsents(message); + expect(result).toEqual({}); + }); + + it('Should return a consents object when consents are given', () => { + const message = { + integrations: { + GA4: { + consents: { + ad_personalization: 'GRANTED', + ad_user_data: 'GRANTED', + }, + }, + }, + }; + const result = prepareUserConsents(message); + expect(result).toEqual({ + ad_personalization: 'GRANTED', + ad_user_data: 'GRANTED', + }); + }); + + it('Should return an empty object when invalid consents are given', () => { + const message = { + integrations: { + GA4: { + consents: { + ad_personalization: 'NOT_SPECIFIED', + ad_user_data: 'NOT_SPECIFIED', + }, + }, + }, + }; + const result = prepareUserConsents(message); + expect(result).toEqual({}); + }); + + it('Should return a valid consents values from consents object', () => { + const message = { + integrations: { + GA4: { + consents: { + ad_personalization: 'NOT_SPECIFIED', + ad_user_data: 'DENIED', + }, + }, + }, + }; + const result = prepareUserConsents(message); + expect(result).toEqual({ + ad_user_data: 'DENIED', + }); + }); + }); }); diff --git a/src/v0/destinations/google_adwords_enhanced_conversions/data/trackConfig.json b/src/v0/destinations/google_adwords_enhanced_conversions/data/trackConfig.json index 562a77f0e8..bf5485270b 100644 --- a/src/v0/destinations/google_adwords_enhanced_conversions/data/trackConfig.json +++ b/src/v0/destinations/google_adwords_enhanced_conversions/data/trackConfig.json @@ -51,7 +51,7 @@ }, { "destKey": "conversionAdjustments[0].userIdentifiers[0].hashedEmail", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false, "metadata": { diff --git a/src/v0/destinations/hs/HSTransform-v2.js b/src/v0/destinations/hs/HSTransform-v2.js index 2acdd82152..3699e1c789 100644 --- a/src/v0/destinations/hs/HSTransform-v2.js +++ b/src/v0/destinations/hs/HSTransform-v2.js @@ -12,7 +12,6 @@ const { defaultPatchRequestConfig, getFieldValueFromMessage, getSuccessRespEvents, - addExternalIdToTraits, defaultBatchRequestConfig, removeUndefinedAndNullValues, getDestinationExternalID, @@ -42,6 +41,7 @@ const { getEventAndPropertiesFromConfig, getHsSearchId, populateTraits, + addExternalIdToHSTraits, } = require('./util'); const { JSON_MIME_TYPE } = require('../../util/constant'); @@ -110,7 +110,7 @@ const processIdentify = async (message, destination, propertyMap) => { GENERIC_TRUE_VALUES.includes(mappedToDestination.toString()) && operation ) { - addExternalIdToTraits(message); + addExternalIdToHSTraits(message); if (!objectType) { throw new InstrumentationError('objectType not found'); } diff --git a/src/v0/destinations/hs/config.js b/src/v0/destinations/hs/config.js index fb9790f0e5..67ad3b5bed 100644 --- a/src/v0/destinations/hs/config.js +++ b/src/v0/destinations/hs/config.js @@ -84,6 +84,9 @@ const RETL_SOURCE = 'rETL'; const mappingConfig = getMappingConfig(ConfigCategory, __dirname); const hsCommonConfigJson = mappingConfig[ConfigCategory.COMMON.name]; +const primaryToSecondaryFields = { + email: 'hs_additional_emails', +}; module.exports = { BASE_ENDPOINT, CONTACT_PROPERTY_MAP_ENDPOINT, @@ -112,5 +115,6 @@ module.exports = { RETL_SOURCE, RETL_CREATE_ASSOCIATION_OPERATION, MAX_CONTACTS_PER_REQUEST, + primaryToSecondaryFields, DESTINATION: 'HS', }; diff --git a/src/v0/destinations/hs/util.js b/src/v0/destinations/hs/util.js index 359c93dc1a..b30207fe15 100644 --- a/src/v0/destinations/hs/util.js +++ b/src/v0/destinations/hs/util.js @@ -1,5 +1,6 @@ /* eslint-disable no-await-in-loop */ const lodash = require('lodash'); +const set = require('set-value'); const get = require('get-value'); const { NetworkInstrumentationError, @@ -20,6 +21,7 @@ const { getDestinationExternalIDInfoForRetl, getValueFromMessage, isNull, + validateEventName, } = require('../../util'); const { CONTACT_PROPERTY_MAP_ENDPOINT, @@ -27,6 +29,7 @@ const { IDENTIFY_CRM_SEARCH_ALL_OBJECTS, SEARCH_LIMIT_VALUE, hsCommonConfigJson, + primaryToSecondaryFields, DESTINATION, MAX_CONTACTS_PER_REQUEST, } = require('./config'); @@ -435,6 +438,7 @@ const getEventAndPropertiesFromConfig = (message, destination, payload) => { if (!hubspotEvents) { throw new InstrumentationError('Event and property mappings are required for track call'); } + validateEventName(event); event = event.trim().toLowerCase(); let eventName; let eventProperties; @@ -574,16 +578,30 @@ const performHubSpotSearch = async ( checkAfter = after; // assigning to the new value if no after we assign it to 0 and no more calls will take place const results = processedResponse.response?.results; + const extraProp = primaryToSecondaryFields[identifierType]; if (results) { searchResults.push( - ...results.map((result) => ({ - id: result.id, - property: result.properties[identifierType], - })), + ...results.map((result) => { + const contact = { + id: result.id, + property: result.properties[identifierType], + }; + // Following maps the extra property to the contact object which + // help us to know if the contact was found using secondary property + if (extraProp) { + contact[extraProp] = result.properties?.[extraProp]; + } + return contact; + }), ); } } - + /* + searchResults = { + id: 'existing_contact_id', + property: 'existing_contact_email', // when email is identifier + hs_additional_emails: ['secondary_email'] // when email is identifier + } */ return searchResults; }; @@ -610,7 +628,25 @@ const getRequestData = (identifierType, chunk) => { limit: SEARCH_LIMIT_VALUE, after: 0, }; - + /* In case of email as identifier we add a filter for hs_additional_emails field + * and append hs_additional_emails to properties list + * We are doing this because there might be emails exisitng as hs_additional_emails for some conatct but + * will not come up in search API until we search with hs_additional_emails as well. + * Not doing this resulted in erro 409 Duplicate records found + */ + const secondaryProp = primaryToSecondaryFields[identifierType]; + if (secondaryProp) { + requestData.filterGroups.push({ + filters: [ + { + propertyName: secondaryProp, + values: chunk, + operator: 'IN', + }, + ], + }); + requestData.properties.push(secondaryProp); + } return requestData; }; @@ -621,7 +657,7 @@ const getRequestData = (identifierType, chunk) => { */ const getExistingContactsData = async (inputs, destination) => { const { Config } = destination; - const updateHubspotIds = []; + const hsIdsToBeUpdated = []; const firstMessage = inputs[0].message; if (!firstMessage) { @@ -649,13 +685,19 @@ const getExistingContactsData = async (inputs, destination) => { destination, ); if (searchResults.length > 0) { - updateHubspotIds.push(...searchResults); + hsIdsToBeUpdated.push(...searchResults); } } - return updateHubspotIds; + return hsIdsToBeUpdated; }; - -const setHsSearchId = (input, id) => { +/** + * This functions sets HsSearchId in the externalId array + * @param {*} input -> Input message + * @param {*} id -> Id to be added + * @param {*} useSecondaryProp -> Let us know if that id was found using secondary property and not primnary + * @returns + */ +const setHsSearchId = (input, id, useSecondaryProp = false) => { const { message } = input; const resultExternalId = []; const externalIdArray = message.context?.externalId; @@ -666,6 +708,11 @@ const setHsSearchId = (input, id) => { if (type.includes(DESTINATION)) { extIdObjParam.hsSearchId = id; } + if (useSecondaryProp) { + // we are using it so that when final payload is made + // then primary key shouldn't be overidden + extIdObjParam.useSecondaryObject = useSecondaryProp; + } resultExternalId.push(extIdObjParam); }); } @@ -678,20 +725,24 @@ const setHsSearchId = (input, id) => { * We do search for all the objects before router transform and assign the type (create/update) * accordingly to context.hubspotOperation * + * For email as primary key we use `hs_additional_emails` as well property to search existing contacts * */ const splitEventsForCreateUpdate = async (inputs, destination) => { // get all the id and properties of already existing objects needed for update. - const updateHubspotIds = await getExistingContactsData(inputs, destination); + const hsIdsToBeUpdated = await getExistingContactsData(inputs, destination); const resultInput = inputs.map((input) => { const { message } = input; const inputParam = input; - const { destinationExternalId } = getDestinationExternalIDInfoForRetl(message, DESTINATION); + const { destinationExternalId, identifierType } = getDestinationExternalIDInfoForRetl( + message, + DESTINATION, + ); - const filteredInfo = updateHubspotIds.filter( + const filteredInfo = hsIdsToBeUpdated.filter( (update) => - update.property.toString().toLowerCase() === destinationExternalId.toString().toLowerCase(), + update.property.toString().toLowerCase() === destinationExternalId.toString().toLowerCase(), // second condition is for secondary property for identifier type ); if (filteredInfo.length > 0) { @@ -699,6 +750,33 @@ const splitEventsForCreateUpdate = async (inputs, destination) => { inputParam.message.context.hubspotOperation = 'updateObject'; return inputParam; } + const secondaryProp = primaryToSecondaryFields[identifierType]; + if (secondaryProp) { + /* second condition is for secondary property for identifier type + For example: + update[secondaryProp] = "abc@e.com;cd@e.com;k@w.com" + destinationExternalId = "cd@e.com" + So we are splitting all the emails in update[secondaryProp] into an array using ';' + and then checking if array includes destinationExternalId + */ + const filteredInfoForSecondaryProp = hsIdsToBeUpdated.filter((update) => + update[secondaryProp] + ?.toString() + .toLowerCase() + .split(';') + .includes(destinationExternalId.toString().toLowerCase()), + ); + if (filteredInfoForSecondaryProp.length > 0) { + inputParam.message.context.externalId = setHsSearchId( + input, + filteredInfoForSecondaryProp[0].id, + true, + ); + inputParam.message.context.hubspotOperation = 'updateObject'; + return inputParam; + } + } + // if not found in the existing contacts, then it's a new contact inputParam.message.context.hubspotOperation = 'createObject'; return inputParam; }); @@ -746,8 +824,22 @@ const populateTraits = async (propertyMap, traits, destination) => { return populatedTraits; }; +const addExternalIdToHSTraits = (message) => { + const externalIdObj = message.context?.externalId?.[0]; + if (externalIdObj.useSecondaryObject) { + /* this condition help us to NOT override the primary key value with the secondary key value + example: + for `email` as primary key and `hs_additonal_emails` as secondary key we don't want to override `email` with `hs_additional_emails`. + neither we want to map anything for `hs_additional_emails` as this property can not be set + */ + return; + } + set(getFieldValueFromMessage(message, 'traits'), externalIdObj.identifierType, externalIdObj.id); +}; + module.exports = { validateDestinationConfig, + addExternalIdToHSTraits, formatKey, fetchFinalSetOfTraits, getProperties, diff --git a/src/v0/destinations/hs/util.test.js b/src/v0/destinations/hs/util.test.js index 30e89d3aee..ea2e10dc3d 100644 --- a/src/v0/destinations/hs/util.test.js +++ b/src/v0/destinations/hs/util.test.js @@ -4,6 +4,7 @@ const { validatePayloadDataTypes, getObjectAndIdentifierType, } = require('./util'); +const { primaryToSecondaryFields } = require('./config'); const propertyMap = { firstName: 'string', @@ -205,7 +206,7 @@ describe('extractUniqueValues utility test cases', () => { describe('getRequestDataAndRequestOptions utility test cases', () => { it('Should return an object with requestData and requestOptions', () => { const identifierType = 'email'; - const chunk = 'test1@gmail.com'; + const chunk = ['test1@gmail.com']; const accessToken = 'dummyAccessToken'; const expectedRequestData = { @@ -219,8 +220,17 @@ describe('getRequestDataAndRequestOptions utility test cases', () => { }, ], }, + { + filters: [ + { + propertyName: primaryToSecondaryFields[identifierType], + values: chunk, + operator: 'IN', + }, + ], + }, ], - properties: [identifierType], + properties: [identifierType, primaryToSecondaryFields[identifierType]], limit: 100, after: 0, }; diff --git a/src/v0/destinations/impact/data/ImpactConversionConfig.json b/src/v0/destinations/impact/data/ImpactConversionConfig.json index 7ae0cc72eb..a898e6a98c 100644 --- a/src/v0/destinations/impact/data/ImpactConversionConfig.json +++ b/src/v0/destinations/impact/data/ImpactConversionConfig.json @@ -120,7 +120,7 @@ }, { "destKey": "CustomerEmail", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/impact/data/ImpactPageLoadConfig.json b/src/v0/destinations/impact/data/ImpactPageLoadConfig.json index 0ca0a26e69..700557d3e0 100644 --- a/src/v0/destinations/impact/data/ImpactPageLoadConfig.json +++ b/src/v0/destinations/impact/data/ImpactPageLoadConfig.json @@ -7,7 +7,7 @@ }, { "destKey": "CustomerEmail", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/impact/util.js b/src/v0/destinations/impact/util.js index b8c6e92e76..8fb43a8df6 100644 --- a/src/v0/destinations/impact/util.js +++ b/src/v0/destinations/impact/util.js @@ -78,6 +78,7 @@ const populateProductProperties = (productsMapping, properties) => { const productProperties = {}; if (products && Array.isArray(products)) { products.forEach((item, index) => { + // Following product properties have a default mapping as well in config.js as itemMapping productProperties[getPropertyName('ItemBrand', index + 1)] = item[getProductsMapping(productsMapping, 'ItemBrand')]; productProperties[getPropertyName('ItemCategory', index + 1)] = @@ -92,8 +93,16 @@ const populateProductProperties = (productsMapping, properties) => { item[getProductsMapping(productsMapping, 'ItemQuantity')]; productProperties[getPropertyName('ItemSku', index + 1)] = item[getProductsMapping(productsMapping, 'ItemSku')]; + + // Following product properties are build from configuration in RudderStack dashboard + if (productsMapping && Array.isArray(productsMapping)) { + productsMapping.forEach((mapping) => { + productProperties[getPropertyName(mapping.to, index + 1)] = item[mapping.from]; + }); + } }); } else { + // Not providing product level mapping here as following are fetched from properties level const index = 1; productProperties[getPropertyName('ItemBrand', index)] = brand; productProperties[getPropertyName('ItemCategory', index)] = category; diff --git a/src/v0/destinations/impact/utils.test.js b/src/v0/destinations/impact/utils.test.js new file mode 100644 index 0000000000..17f90bbc5d --- /dev/null +++ b/src/v0/destinations/impact/utils.test.js @@ -0,0 +1,134 @@ +const { populateProductProperties } = require('./util'); +// Generated by CodiumAI + +describe('populateProductProperties', () => { + // Given a valid productsMapping and properties, it should return product properties with default mappings and configured mappings from RudderStack dashboard + it('should return product properties with default and configured mappings', () => { + // Arrange + const productsMapping = [ + { to: 'ItemBrand', from: 'Brand' }, + { to: 'ItemCategory', from: 'Category' }, + { to: 'ItemName', from: 'Name' }, + { to: 'ItemPrice', from: 'Price' }, + { to: 'ItemPromoCode', from: 'PromoCode' }, + { to: 'ItemQuantity', from: 'Quantity' }, + { to: 'ItemSku', from: 'Sku' }, + { to: 'dummyRHS', from: 'dummyLHS' }, + ]; + const properties = { + products: [ + { + Brand: 'Brand1', + Category: 'Category1', + Name: 'Name1', + Price: 10, + PromoCode: 'PromoCode1', + Quantity: 1, + Sku: 'Sku1', + dummyLHS: 'DummyValue', + }, + { + Brand: 'Brand2', + Category: 'Category2', + Name: 'Name2', + Price: 20, + PromoCode: 'PromoCode2', + Quantity: 2, + }, + ], + brand: 'Brand3', + category: 'Category3', + name: 'Name3', + price: 30, + coupon: 'PromoCode3', + quantity: 3, + sku: 'Sku3', + }; + + // Act + const result = populateProductProperties(productsMapping, properties); + + // Assert + expect(result).toEqual({ + ItemBrand1: 'Brand1', + ItemCategory1: 'Category1', + ItemName1: 'Name1', + ItemPrice1: 10, + ItemPromoCode1: 'PromoCode1', + ItemQuantity1: 1, + ItemSku1: 'Sku1', + ItemBrand2: 'Brand2', + ItemCategory2: 'Category2', + ItemName2: 'Name2', + ItemPrice2: 20, + ItemPromoCode2: 'PromoCode2', + ItemQuantity2: 2, + dummyRHS1: 'DummyValue', + }); + }); + + // Given an empty productsMapping and valid properties, it should return product properties with default mappings from properties + it('should return product properties with default mappings from properties when products array is not available', () => { + // Arrange + const productsMapping = []; + const properties = { + brand: 'Brand3', + category: 'Category3', + name: 'Name3', + price: 30, + coupon: 'PromoCode3', + quantity: 3, + sku: 'Sku3', + }; + + // Act + const result = populateProductProperties(productsMapping, properties); + + // Assert + expect(result).toEqual({ + ItemBrand1: 'Brand3', + ItemCategory1: 'Category3', + ItemName1: 'Name3', + ItemPrice1: 30, + ItemPromoCode1: 'PromoCode3', + ItemQuantity1: 3, + ItemSku1: 'Sku3', + }); + }); + it('should return product properties with custom mappings', () => { + const productsMapping = [ + { + from: 'dummy_LHS', + to: 'dummy_RHS', + }, + ]; + const properties = { + products: [ + { + brand: 'Brand3', + category: 'Category3', + name: 'Name3', + price: 30, + coupon: 'PromoCode3', + quantity: 3, + sku: 'Sku3', + dummy_LHS: 'DummyValue', + }, + ], + }; + + const result = populateProductProperties(productsMapping, properties); + + // Assert + expect(result).toEqual({ + ItemBrand1: 'Brand3', + ItemCategory1: 'Category3', + ItemName1: 'Name3', + ItemPrice1: 30, + ItemPromoCode1: 'PromoCode3', + ItemQuantity1: 3, + ItemSku1: 'Sku3', + dummy_RHS1: 'DummyValue', + }); + }); +}); diff --git a/src/v0/destinations/klaviyo/data/KlaviyoGroup.json b/src/v0/destinations/klaviyo/data/KlaviyoGroup.json index baf6bcee86..b03cc9ee0a 100644 --- a/src/v0/destinations/klaviyo/data/KlaviyoGroup.json +++ b/src/v0/destinations/klaviyo/data/KlaviyoGroup.json @@ -1,7 +1,7 @@ [ { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json b/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json index b358919bc1..fd46b6cda9 100644 --- a/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json +++ b/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json @@ -7,7 +7,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/klaviyo/data/KlaviyoProfile.json b/src/v0/destinations/klaviyo/data/KlaviyoProfile.json index 329ecd978f..03f155e787 100644 --- a/src/v0/destinations/klaviyo/data/KlaviyoProfile.json +++ b/src/v0/destinations/klaviyo/data/KlaviyoProfile.json @@ -1,7 +1,7 @@ [ { "destKey": "$email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/mailchimp/utils.js b/src/v0/destinations/mailchimp/utils.js index 1f4fc03ee5..a726f23a39 100644 --- a/src/v0/destinations/mailchimp/utils.js +++ b/src/v0/destinations/mailchimp/utils.js @@ -1,9 +1,12 @@ const get = require('get-value'); const md5 = require('md5'); -const { InstrumentationError, NetworkError } = require('@rudderstack/integrations-lib'); +const { + InstrumentationError, + NetworkError, + structuredLogger: logger, +} = require('@rudderstack/integrations-lib'); const myAxios = require('../../../util/myAxios'); const { MappedToDestinationKey } = require('../../../constants'); -const logger = require('../../../logger'); const { isDefinedAndNotNull, isDefined, diff --git a/src/v0/destinations/mailjet/data/MailJetIdentifyConfig.json b/src/v0/destinations/mailjet/data/MailJetIdentifyConfig.json index e32463e034..106560a447 100644 --- a/src/v0/destinations/mailjet/data/MailJetIdentifyConfig.json +++ b/src/v0/destinations/mailjet/data/MailJetIdentifyConfig.json @@ -1,7 +1,7 @@ [ { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": true, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js b/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js index aa4b3aacc4..13e1b3a09a 100644 --- a/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js +++ b/src/v0/destinations/marketo_bulk_upload/marketo_bulk_upload.util.test.js @@ -514,7 +514,7 @@ describe('checkEventStatusViaSchemaMatching', () => { }); // The function correctly handles events with null values. - it('should correctly handle events with null values', () => { + it('should ignore event properties with null values', () => { const event = { input: [ { @@ -537,8 +537,6 @@ describe('checkEventStatusViaSchemaMatching', () => { const result = checkEventStatusViaSchemaMatching(event, fieldSchemaMapping); - expect(result).toEqual({ - job1: 'invalid id', - }); + expect(result).toEqual({}); }); }); diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js index 4c99ba7483..033239b5e4 100644 --- a/src/v0/destinations/marketo_bulk_upload/util.js +++ b/src/v0/destinations/marketo_bulk_upload/util.js @@ -3,6 +3,7 @@ const { RetryableError, NetworkError, TransformationError, + isDefinedAndNotNull, } = require('@rudderstack/integrations-lib'); const { handleHttpRequest } = require('../../../adapters/network'); const tags = require('../../util/tags'); @@ -360,7 +361,6 @@ const getFieldSchemaMap = async (accessToken, munchkinId) => { module: 'router', }, ); - if (fieldSchemaMapping.response.errors) { handleCommonErrorResponse( fieldSchemaMapping, @@ -411,7 +411,11 @@ const checkEventStatusViaSchemaMatching = (event, fieldMap) => { const expectedDataType = SCHEMA_DATA_TYPE_MAP[fieldMap[paramName]]; const actualDataType = typeof paramValue; - if (!mismatchedFields[job_id] && actualDataType !== expectedDataType) { + if ( + isDefinedAndNotNull(paramValue) && + !mismatchedFields[job_id] && + actualDataType !== expectedDataType + ) { mismatchedFields[job_id] = `invalid ${paramName}`; } }); diff --git a/src/v0/destinations/monday/util.js b/src/v0/destinations/monday/util.js index 872fad42a7..0694028eb2 100644 --- a/src/v0/destinations/monday/util.js +++ b/src/v0/destinations/monday/util.js @@ -27,7 +27,7 @@ const getGroupId = (groupTitle, board) => { } }); if (groupId) { - return groupId; + return JSON.stringify(groupId); } throw new ConfigurationError(`Group ${groupTitle} doesn't exist in the board`); }; @@ -239,19 +239,20 @@ const populatePayload = (message, Config, boardDeatailsResponse) => { columnToPropertyMapping, boardDeatailsResponse.response?.data, ); + const items = [ + `board_id: ${boardId}`, + `item_name: ${JSON.stringify(message.properties?.name)}`, + `column_values: ${JSON.stringify(columnValues)}`, + ]; if (groupTitle) { if (!message.properties?.name) { throw new InstrumentationError('Item name is required to create an item'); } const groupId = getGroupId(groupTitle, boardDeatailsResponse.response?.data); - payload.query = `mutation { create_item (board_id: ${boardId}, group_id: ${groupId} item_name: ${JSON.stringify( - message.properties?.name, - )}, column_values: ${JSON.stringify(columnValues)}) {id}}`; - } else { - payload.query = `mutation { create_item (board_id: ${boardId}, item_name: ${JSON.stringify( - message.properties?.name, - )}, column_values: ${JSON.stringify(columnValues)}) {id}}`; + items.push(`group_id: ${groupId}`); } + const itemsQuery = items.join(', '); + payload.query = `mutation { create_item (${itemsQuery}) {id}}`; return payload; }; diff --git a/src/v0/destinations/mp/data/MPIdentifyConfig.json b/src/v0/destinations/mp/data/MPIdentifyConfig.json index 679ce66d7f..b5e4f51970 100644 --- a/src/v0/destinations/mp/data/MPIdentifyConfig.json +++ b/src/v0/destinations/mp/data/MPIdentifyConfig.json @@ -7,7 +7,7 @@ }, { "destKey": "$email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/mp/transform.js b/src/v0/destinations/mp/transform.js index a2c40a5672..09a7862f9a 100644 --- a/src/v0/destinations/mp/transform.js +++ b/src/v0/destinations/mp/transform.js @@ -24,8 +24,8 @@ const { mappingConfig, BASE_ENDPOINT, BASE_ENDPOINT_EU, - IMPORT_MAX_BATCH_SIZE, TRACK_MAX_BATCH_SIZE, + IMPORT_MAX_BATCH_SIZE, ENGAGE_MAX_BATCH_SIZE, GROUPS_MAX_BATCH_SIZE, } = require('./config'); @@ -109,6 +109,7 @@ const responseBuilderSimple = (payload, message, eventType, destConfig) => { strict: credentials.params.strict, }; break; + default: response.endpoint = dataResidency === 'eu' ? `${BASE_ENDPOINT_EU}/engage/` : `${BASE_ENDPOINT}/engage/`; diff --git a/src/v0/destinations/pinterest_tag/data/pinterestUserConfig.json b/src/v0/destinations/pinterest_tag/data/pinterestUserConfig.json index 0ab963ac3c..b95f539e7c 100644 --- a/src/v0/destinations/pinterest_tag/data/pinterestUserConfig.json +++ b/src/v0/destinations/pinterest_tag/data/pinterestUserConfig.json @@ -1,7 +1,7 @@ [ { "destKey": "em", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/revenue_cat/data/RCIdentifyConfig.json b/src/v0/destinations/revenue_cat/data/RCIdentifyConfig.json index f7e6fa7acc..b31f1355a6 100644 --- a/src/v0/destinations/revenue_cat/data/RCIdentifyConfig.json +++ b/src/v0/destinations/revenue_cat/data/RCIdentifyConfig.json @@ -13,7 +13,7 @@ }, { "destKey": "$email.value", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/rockerbox/data/RockerboxTrackConfig.json b/src/v0/destinations/rockerbox/data/RockerboxTrackConfig.json index e405f1477d..cc3a4d5ec5 100644 --- a/src/v0/destinations/rockerbox/data/RockerboxTrackConfig.json +++ b/src/v0/destinations/rockerbox/data/RockerboxTrackConfig.json @@ -12,7 +12,7 @@ "sourceFromGenericMap": false }, { - "sourceKeys": "email", + "sourceKeys": "emailOnly", "destKey": "email", "required": false, "sourceFromGenericMap": true diff --git a/src/v0/destinations/sendgrid/data/SendgridIdentify.json b/src/v0/destinations/sendgrid/data/SendgridIdentify.json index e70378dd28..bfafd0da01 100644 --- a/src/v0/destinations/sendgrid/data/SendgridIdentify.json +++ b/src/v0/destinations/sendgrid/data/SendgridIdentify.json @@ -1,7 +1,7 @@ [ { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": true, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/serenytics/data/SerenyticsIdentifyConfig.json b/src/v0/destinations/serenytics/data/SerenyticsIdentifyConfig.json index 4c50cb4a54..9161a7c698 100644 --- a/src/v0/destinations/serenytics/data/SerenyticsIdentifyConfig.json +++ b/src/v0/destinations/serenytics/data/SerenyticsIdentifyConfig.json @@ -35,7 +35,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/destinations/sfmc/transform.js b/src/v0/destinations/sfmc/transform.js index 53925bc7ed..bf474ff3f0 100644 --- a/src/v0/destinations/sfmc/transform.js +++ b/src/v0/destinations/sfmc/transform.js @@ -7,8 +7,8 @@ const { isDefinedAndNotNull, isEmpty, } = require('@rudderstack/integrations-lib'); -const myAxios = require('../../../util/myAxios'); const { EventType } = require('../../../constants'); +const { handleHttpRequest } = require('../../../adapters/network'); const { CONFIG_CATEGORIES, MAPPING_CONFIG, ENDPOINTS } = require('./config'); const { removeUndefinedAndNullValues, @@ -22,10 +22,8 @@ const { getHashFromArray, simpleProcessRouterDest, } = require('../../util'); -const { - getDynamicErrorType, - nodeSysErrorToStatus, -} = require('../../../adapters/utils/networkUtils'); +const { getDynamicErrorType } = require('../../../adapters/utils/networkUtils'); +const { isHttpStatusSuccess } = require('../../util'); const tags = require('../../util/tags'); const { JSON_MIME_TYPE } = require('../../util/constant'); @@ -34,51 +32,38 @@ const CONTACT_KEY_KEY = 'Contact Key'; // DOC: https://developer.salesforce.com/docs/atlas.en-us.mc-app-development.meta/mc-app-development/access-token-s2s.htm const getToken = async (clientId, clientSecret, subdomain) => { - try { - const resp = await myAxios.post( - `https://${subdomain}.${ENDPOINTS.GET_TOKEN}`, - { - grant_type: 'client_credentials', - client_id: clientId, - client_secret: clientSecret, - }, - { - 'Content-Type': JSON_MIME_TYPE, - }, - { - destType: 'sfmc', - feature: 'transformation', - endpointPath: '/token', - requestMethod: 'POST', - module: 'router', - }, - ); - if (resp && resp.data) { - return resp.data.access_token; - } - const status = resp.status || 400; + const { processedResponse: processedResponseSfmc } = await handleHttpRequest( + 'post', + `https://${subdomain}.${ENDPOINTS.GET_TOKEN}`, + { + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret, + }, + { + 'Content-Type': JSON_MIME_TYPE, + }, + { + destType: 'sfmc', + feature: 'transformation', + endpointPath: '/token', + requestMethod: 'POST', + module: 'router', + }, + ); + + if (!isHttpStatusSuccess(processedResponseSfmc.status)) { throw new NetworkError( 'Could not retrieve access token', - status, + processedResponseSfmc.status || 400, { - [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(processedResponseSfmc.status || 400), }, - resp, + processedResponseSfmc.response, ); - } catch (error) { - if (!isEmpty(error.response)) { - const status = error.status || 400; - throw new NetworkError(`Authorization Failed ${error.response.statusText}`, status, { - [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), - }); - } else { - const httpError = nodeSysErrorToStatus(error.code); - const status = httpError.status || 400; - throw new NetworkError(`Authorization Failed ${httpError.message}`, status, { - [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), - }); - } } + + return processedResponseSfmc.response.access_token; }; // DOC : https://developer.salesforce.com/docs/atlas.en-us.noversion.mc-apis.meta/mc-apis/createContacts.htm diff --git a/src/v0/destinations/snapchat_conversion/config.js b/src/v0/destinations/snapchat_conversion/config.js index e0126ea3b1..1cce713fbb 100644 --- a/src/v0/destinations/snapchat_conversion/config.js +++ b/src/v0/destinations/snapchat_conversion/config.js @@ -55,6 +55,7 @@ const eventNameMapping = { save: 'SAVE', subscribe: 'SUBSCRIBE', complete_tutorial: 'COMPLETE_TUTORIAL', + level_complete: 'LEVEL_COMPLETE', invite: 'INVITE', login: 'LOGIN', share: 'SHARE', diff --git a/src/v0/destinations/user/data/USERGroupConfig.json b/src/v0/destinations/user/data/USERGroupConfig.json index c87653442d..c31020b68f 100644 --- a/src/v0/destinations/user/data/USERGroupConfig.json +++ b/src/v0/destinations/user/data/USERGroupConfig.json @@ -17,7 +17,7 @@ "required": true }, { - "sourceKeys": "email", + "sourceKeys": "emailOnly", "destKey": "email", "required": false, "sourceFromGenericMap": true diff --git a/src/v0/destinations/user/data/USERIdentifyConfig.json b/src/v0/destinations/user/data/USERIdentifyConfig.json index 2b89c2a3a6..f87bdd01fd 100644 --- a/src/v0/destinations/user/data/USERIdentifyConfig.json +++ b/src/v0/destinations/user/data/USERIdentifyConfig.json @@ -7,7 +7,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "required": false, "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/vero/data/VeroIdentifyConfig.json b/src/v0/destinations/vero/data/VeroIdentifyConfig.json index 8ea456cc1c..8ec589aaed 100644 --- a/src/v0/destinations/vero/data/VeroIdentifyConfig.json +++ b/src/v0/destinations/vero/data/VeroIdentifyConfig.json @@ -5,7 +5,7 @@ "sourceFromGenericMap": true }, { - "sourceKeys": "email", + "sourceKeys": "emailOnly", "destKey": "email", "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/vero/data/VeroTrackConfig.json b/src/v0/destinations/vero/data/VeroTrackConfig.json index a3a23a6e66..04974c5eb2 100644 --- a/src/v0/destinations/vero/data/VeroTrackConfig.json +++ b/src/v0/destinations/vero/data/VeroTrackConfig.json @@ -5,7 +5,7 @@ "sourceFromGenericMap": true }, { - "sourceKeys": "email", + "sourceKeys": "emailOnly", "destKey": "identity.email", "sourceFromGenericMap": true }, diff --git a/src/v0/destinations/webengage/data/WEBENGAGEIdentifyConfig.json b/src/v0/destinations/webengage/data/WEBENGAGEIdentifyConfig.json index a99fc47a3f..4e04703fc0 100644 --- a/src/v0/destinations/webengage/data/WEBENGAGEIdentifyConfig.json +++ b/src/v0/destinations/webengage/data/WEBENGAGEIdentifyConfig.json @@ -55,7 +55,7 @@ }, { "destKey": "email", - "sourceKeys": "email", + "sourceKeys": "emailOnly", "sourceFromGenericMap": true, "required": false }, diff --git a/src/v0/sources/canny/transform.js b/src/v0/sources/canny/transform.js index 9188f5ac34..38ed5e137e 100644 --- a/src/v0/sources/canny/transform.js +++ b/src/v0/sources/canny/transform.js @@ -2,7 +2,6 @@ const sha256 = require('sha256'); const { TransformationError } = require('@rudderstack/integrations-lib'); const Message = require('../message'); const { voterMapping, authorMapping, checkForRequiredFields } = require('./util'); -const { logger } = require('../../../logger'); const CannyOperation = { VOTE_CREATED: 'vote.created', @@ -15,7 +14,7 @@ const CannyOperation = { * @param {*} event * @param {*} typeOfUser */ -function settingIds(message, event, typeOfUser) { +function settingIds(message, event, typeOfUser, logger) { const clonedMessage = { ...message }; try { // setting up userId @@ -48,7 +47,7 @@ function settingIds(message, event, typeOfUser) { * @param {*} typeOfUser * @returns message */ -function createMessage(event, typeOfUser) { +function createMessage(event, typeOfUser, logger) { const message = new Message(`Canny`); message.setEventType('track'); @@ -61,7 +60,7 @@ function createMessage(event, typeOfUser) { message.context.integration.version = '1.0.0'; - const finalMessage = settingIds(message, event, typeOfUser); + const finalMessage = settingIds(message, event, typeOfUser, logger); checkForRequiredFields(finalMessage); @@ -73,7 +72,7 @@ function createMessage(event, typeOfUser) { return finalMessage; } -function process(event) { +function process(event, logger) { let typeOfUser; switch (event.type) { @@ -86,6 +85,6 @@ function process(event) { typeOfUser = 'author'; } - return createMessage(event, typeOfUser); + return createMessage(event, typeOfUser, logger); } module.exports = { process }; diff --git a/src/v0/sources/shopify/transform.js b/src/v0/sources/shopify/transform.js index 93e3ed0c72..4886fb3df1 100644 --- a/src/v0/sources/shopify/transform.js +++ b/src/v0/sources/shopify/transform.js @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ const lodash = require('lodash'); const get = require('get-value'); +const { RedisError } = require('@rudderstack/integrations-lib'); const stats = require('../../../util/stats'); const { getShopifyTopic, @@ -15,7 +16,6 @@ const { const { RedisDB } = require('../../../util/redis/redisConnector'); const { removeUndefinedAndNullValues, isDefinedAndNotNull } = require('../../util'); const Message = require('../message'); -const logger = require('../../../logger'); const { EventType } = require('../../../constants'); const { INTEGERATION, @@ -205,7 +205,7 @@ const processEvent = async (inputEvent, metricMetadata) => { }; const isIdentifierEvent = (event) => ['rudderIdentifier', 'rudderSessionIdentifier'].includes(event?.event); -const processIdentifierEvent = async (event, metricMetadata) => { +const processIdentifierEvent = async (event, metricMetadata, logger) => { if (useRedisDatabase) { let value; let field; @@ -249,24 +249,19 @@ const processIdentifierEvent = async (event, metricMetadata) => { source: metricMetadata.source, writeKey: metricMetadata.writeKey, }); + // returning 500 as status code in case of redis failure + throw new RedisError(`${e}`, 500); } } - const result = { - outputToSource: { - body: Buffer.from('OK').toString('base64'), - contentType: 'text/plain', - }, - statusCode: 200, - }; - return result; + return NO_OPERATION_SUCCESS; }; -const process = async (event) => { +const process = async (event, logger) => { const metricMetadata = { writeKey: event.query_parameters?.writeKey?.[0], source: 'SHOPIFY', }; if (isIdentifierEvent(event)) { - return processIdentifierEvent(event, metricMetadata); + return processIdentifierEvent(event, metricMetadata, logger); } const response = await processEvent(event, metricMetadata); return response; diff --git a/src/v0/sources/shopify/util.js b/src/v0/sources/shopify/util.js index c4bbb61b9c..3dc54cc434 100644 --- a/src/v0/sources/shopify/util.js +++ b/src/v0/sources/shopify/util.js @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ const { v5 } = require('uuid'); const sha256 = require('sha256'); -const { TransformationError } = require('@rudderstack/integrations-lib'); +const { TransformationError, structuredLogger: logger } = require('@rudderstack/integrations-lib'); const stats = require('../../../util/stats'); const { constructPayload, @@ -12,7 +12,6 @@ const { isDefinedAndNotNull, } = require('../../util'); const { RedisDB } = require('../../../util/redis/redisConnector'); -const logger = require('../../../logger'); const { lineItemsMappingJSON, productMappingJSON, diff --git a/src/v0/util/data/GenericFieldMapping.json b/src/v0/util/data/GenericFieldMapping.json index 87dd5e5e55..0a7b309d89 100644 --- a/src/v0/util/data/GenericFieldMapping.json +++ b/src/v0/util/data/GenericFieldMapping.json @@ -116,5 +116,11 @@ "context.traits.address.postal_code", "context.traits.address.postalCode" ], - "sessionId": ["session_id", "context.sessionId"] + "sessionId": ["session_id", "context.sessionId"], + "countryCode": [ + "traits.countryCode", + "traits.address.countryCode", + "context.traits.address.countryCode", + "context.traits.countryCode" + ] } diff --git a/src/v0/util/googleUtils/index.js b/src/v0/util/googleUtils/index.js index c153731e73..ef7c244c17 100644 --- a/src/v0/util/googleUtils/index.js +++ b/src/v0/util/googleUtils/index.js @@ -1,4 +1,5 @@ const GOOGLE_ALLOWED_CONSENT_STATUS = ['UNSPECIFIED', 'UNKNOWN', 'GRANTED', 'DENIED']; +const GA4_ALLOWED_CONSENT_STATUS = ['GRANTED', 'DENIED']; const UNSPECIFIED_CONSENT = 'UNSPECIFIED'; const UNKNOWN_CONSENT = 'UNKNOWN'; @@ -82,10 +83,36 @@ const finaliseConsent = (consentConfigMap, eventLevelConsent = {}, destConfig = return consentObj; }; +/** + * Populates the consent object based on the provided configuration and consent mapping. + * @param {*} consentConfigMap + * @param {*} eventLevelConsent + * @returns + */ +const finaliseAnalyticsConsents = (consentConfigMap, eventLevelConsent = {}) => { + const consentObj = {}; + // Iterate through each key in consentConfigMap to set the consent + Object.keys(consentConfigMap).forEach((configKey) => { + const consentKey = consentConfigMap[configKey]; // e.g., 'ad_user_data' + + // Set consent only if valid + if ( + eventLevelConsent && + eventLevelConsent.hasOwnProperty(consentKey) && + GA4_ALLOWED_CONSENT_STATUS.includes(eventLevelConsent[consentKey]) + ) { + consentObj[consentKey] = eventLevelConsent[consentKey]; + } + }); + + return consentObj; +}; + module.exports = { populateConsentFromConfig, UNSPECIFIED_CONSENT, UNKNOWN_CONSENT, GOOGLE_ALLOWED_CONSENT_STATUS, finaliseConsent, + finaliseAnalyticsConsents, }; diff --git a/src/v0/util/googleUtils/index.test.js b/src/v0/util/googleUtils/index.test.js index 28e0fa9ac8..76ec624311 100644 --- a/src/v0/util/googleUtils/index.test.js +++ b/src/v0/util/googleUtils/index.test.js @@ -1,4 +1,8 @@ -const { populateConsentFromConfig, finaliseConsent } = require('./index'); +const { + finaliseConsent, + populateConsentFromConfig, + finaliseAnalyticsConsents, +} = require('./index'); describe('unit test for populateConsentFromConfig', () => { const consentConfigMap = { @@ -243,3 +247,49 @@ describe('finaliseConsent', () => { }); }); }); + +describe('unit test for finaliseAnalyticsConsents', () => { + const consentConfigMap = { + personalizationConsent: 'ad_personalization', + userDataConsent: 'ad_user_data', + }; + it('Should return an empty object when no valid consents are provided', () => { + const result = finaliseAnalyticsConsents(consentConfigMap, {}); + expect(result).toEqual({}); + }); + + it('Should set ad_user_data property of consent object when userDataConsent property is provided and its value is one of the allowed consent statuses', () => { + const properties = { ad_user_data: 'GRANTED' }; + const result = finaliseAnalyticsConsents(consentConfigMap, properties); + expect(result).toEqual({ ad_user_data: 'GRANTED' }); + }); + + it('Should set ad_personalization property of consent object when personalizationConsent property is provided and its value is one of the allowed consent statuses', () => { + const properties = { ad_personalization: 'DENIED' }; + const result = finaliseAnalyticsConsents(consentConfigMap, properties); + expect(result).toEqual({ ad_personalization: 'DENIED' }); + }); + + it('Should return an empty object when properties parameter is not provided', () => { + const result = finaliseAnalyticsConsents(consentConfigMap, undefined); + expect(result).toEqual({}); + }); + + it('Should return an empty object when properties parameter is null', () => { + const result = finaliseAnalyticsConsents(consentConfigMap, null); + expect(result).toEqual({}); + }); + + it('Should return an empty object when properties parameter is an UNSPECIFIED object', () => { + const result = finaliseAnalyticsConsents(consentConfigMap, {}); + expect(result).toEqual({}); + }); + + it('should return empty object when properties parameter contains ad_user_data and ad_personalization with non-allowed values', () => { + const result = finaliseAnalyticsConsents(consentConfigMap, { + userDataConsent: 'RANDOM', + personalizationConsent: 'RANDOM', + }); + expect(result).toEqual({}); + }); +}); diff --git a/src/v0/util/index.js b/src/v0/util/index.js index 32872cc5d9..ac1bacf404 100644 --- a/src/v0/util/index.js +++ b/src/v0/util/index.js @@ -1328,12 +1328,19 @@ const generateExclusionList = (mappingConfig) => */ function extractCustomFields(message, payload, keys, exclusionFields) { const mappingKeys = []; + // Define reserved words + const reservedWords = ['__proto__', 'constructor', 'prototype']; + + const isReservedWord = (key) => reservedWords.includes(key); + if (Array.isArray(keys)) { keys.forEach((key) => { const messageContext = get(message, key); if (messageContext) { Object.keys(messageContext).forEach((k) => { - if (!exclusionFields.includes(k)) mappingKeys.push(k); + if (!exclusionFields.includes(k) && !isReservedWord(k)) { + mappingKeys.push(k); + } }); mappingKeys.forEach((mappingKey) => { if (!(typeof messageContext[mappingKey] === 'undefined')) { @@ -1344,7 +1351,9 @@ function extractCustomFields(message, payload, keys, exclusionFields) { }); } else if (keys === 'root') { Object.keys(message).forEach((k) => { - if (!exclusionFields.includes(k)) mappingKeys.push(k); + if (!exclusionFields.includes(k) && !isReservedWord(k)) { + mappingKeys.push(k); + } }); mappingKeys.forEach((mappingKey) => { if (!(typeof message[mappingKey] === 'undefined')) { diff --git a/src/v0/util/index.test.js b/src/v0/util/index.test.js index 810eb5a9d4..c34d513325 100644 --- a/src/v0/util/index.test.js +++ b/src/v0/util/index.test.js @@ -506,3 +506,187 @@ describe('validateEventAndLowerCaseConversion Tests', () => { }).toThrow(InstrumentationError); }); }); + +describe('extractCustomFields', () => { + // Handle reserved words in message keys + it('should handle reserved word "prototype" in message keys when keys are provided', () => { + const message = { + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + prototype: 'reserved', + }, + context: { + traits: { + phone: '1234567890', + city: 'New York', + country: 'USA', + prototype: 'reserved', + }, + }, + properties: { + title: 'Developer', + organization: 'ABC Company', + zip: '12345', + prototype: 'reserved', + }, + }; + + const payload = {}; + + const keys = ['properties', 'context.traits', 'traits']; + + const exclusionFields = [ + 'firstName', + 'lastName', + 'phone', + 'title', + 'organization', + 'city', + 'region', + 'country', + 'zip', + 'image', + 'timezone', + ]; + + const result = utilities.extractCustomFields(message, payload, keys, exclusionFields); + + expect(result).toEqual({ + email: 'john.doe@example.com', + }); + }); + + it('should handle reserved word "__proto__" in message keys when keys are provided', () => { + const message = { + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + __proto__: 'reserved', + }, + context: { + traits: { + phone: '1234567890', + city: 'New York', + country: 'USA', + __proto__: 'reserved', + }, + }, + properties: { + title: 'Developer', + organization: 'ABC Company', + zip: '12345', + __proto__: 'reserved', + }, + }; + + const payload = {}; + + const keys = ['properties', 'context.traits', 'traits']; + + const exclusionFields = [ + 'firstName', + 'lastName', + 'phone', + 'title', + 'organization', + 'city', + 'region', + 'country', + 'zip', + 'image', + 'timezone', + ]; + const result = utilities.extractCustomFields(message, payload, keys, exclusionFields); + expect(result).toEqual({ + email: 'john.doe@example.com', + }); + }); + + it('should handle reserved word "constructor" in message keys when keys are provided', () => { + const message = { + traits: { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + constructor: 'reserved', + }, + context: { + traits: { + phone: '1234567890', + city: 'New York', + country: 'USA', + constructor: 'reserved', + }, + }, + properties: { + title: 'Developer', + organization: 'ABC Company', + zip: '12345', + constructor: 'reserved', + }, + }; + + const payload = {}; + + const keys = ['properties', 'context.traits', 'traits']; + + const exclusionFields = [ + 'firstName', + 'lastName', + 'phone', + 'title', + 'organization', + 'city', + 'region', + 'country', + 'zip', + 'image', + 'timezone', + ]; + const result = utilities.extractCustomFields(message, payload, keys, exclusionFields); + expect(result).toEqual({ + email: 'john.doe@example.com', + }); + }); + + it('should handle reserved words in message keys when key is root', () => { + const message = { + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + prototype: 'reserved', + phone: '1234567890', + city: 'New York', + country: 'USA', + __proto__: 'reserved', + constructor: 'reserved', + }; + + const payload = {}; + + const keys = 'root'; + + const exclusionFields = [ + 'firstName', + 'lastName', + 'phone', + 'title', + 'organization', + 'city', + 'region', + 'country', + 'zip', + 'image', + 'timezone', + ]; + + const result = utilities.extractCustomFields(message, payload, keys, exclusionFields); + + expect(result).toEqual({ + email: 'john.doe@example.com', + }); + }); +}); diff --git a/src/v1/destinations/bloomreach/networkHandler.js b/src/v1/destinations/bloomreach/networkHandler.js new file mode 100644 index 0000000000..a3c17a167b --- /dev/null +++ b/src/v1/destinations/bloomreach/networkHandler.js @@ -0,0 +1,83 @@ +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { isHttpStatusSuccess } = require('../../../v0/util/index'); +const tags = require('../../../v0/util/tags'); + +// { +// "results": [ +// { +// "success": true +// }, +// { +// "success": false, +// "errors": [ +// "At least one id should be specified." +// ] +// } +// ], +// "start_time": 1710750816.8504393, +// "end_time": 1710750816.8518236, +// "success": true +// } +const checkIfEventIsAbortableAndExtractErrorMessage = (element) => { + if (element.success) { + return { isAbortable: false, errorMsg: '' }; + } + + const errorMsg = element.errors.join(', '); + return { isAbortable: true, errorMsg }; +}; + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; + + const message = '[BLOOMREACH Response V1 Handler] - Request Processed Successfully'; + const responseWithIndividualEvents = []; + const { response, status } = destinationResponse; + + if (isHttpStatusSuccess(status)) { + // check for Partial Event failures and Successes + const { results } = response; + results.forEach((event, idx) => { + const proxyOutput = { + statusCode: 200, + metadata: rudderJobMetadata[idx], + error: 'success', + }; + // update status of partial event if abortable + const { isAbortable, errorMsg } = checkIfEventIsAbortableAndExtractErrorMessage(event); + if (isAbortable) { + proxyOutput.statusCode = 400; + proxyOutput.error = errorMsg; + } + responseWithIndividualEvents.push(proxyOutput); + }); + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; + } + throw new TransformerProxyError( + `BLOOMREACH: Error encountered in transformer proxy V1`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + '', + responseWithIndividualEvents, + ); +}; +function networkHandler() { + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.prepareProxy = prepareProxyRequest; + this.responseHandler = responseHandler; +} +module.exports = { networkHandler }; diff --git a/src/v1/destinations/linkedin_ads/networkHandler.js b/src/v1/destinations/linkedin_ads/networkHandler.js new file mode 100644 index 0000000000..8219e18fcb --- /dev/null +++ b/src/v1/destinations/linkedin_ads/networkHandler.js @@ -0,0 +1,112 @@ +const lodash = require('lodash'); +const { TransformerProxyError } = require('../../../v0/util/errorTypes'); +const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network'); +const { isHttpStatusSuccess } = require('../../../v0/util/index'); + +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const tags = require('../../../v0/util/tags'); +const { + constructPartialStatus, + createResponseArray, + getAuthErrCategoryFromStCode, +} = require('../../../cdk/v2/destinations/linkedin_ads/utils'); + +// eslint-disable-next-line consistent-return +// ref : +// 1) https://learn.microsoft.com/en-us/linkedin/shared/api-guide/concepts/error-handling +// 2) https://learn.microsoft.com/en-us/linkedin/marketing/integrations/ads-reporting/conversions-api?view=li-lms-2024-02&tabs=http#api-error-details +// statusCode : 422 we have found by trial and error, not documented in their doc + +const responseHandler = (responseParams) => { + const { destinationResponse, rudderJobMetadata } = responseParams; + const message = `[LINKEDIN_CONVERSION_API Response V1 Handler] - Request Processed Successfully`; + let responseWithIndividualEvents = []; + const { response, status } = destinationResponse; + + // even if a single event is unsuccessful, the entire batch will fail, we will filter that event out and retry others + if (!isHttpStatusSuccess(status)) { + const errorMessage = response.message || 'unknown error format'; + responseWithIndividualEvents = rudderJobMetadata.map((metadata) => ({ + statusCode: status, + metadata, + error: errorMessage, + })); + if (status === 401 || status === 403) { + const finalStatus = status === 401 && response.code !== 'REVOKED_ACCESS_TOKEN' ? 500 : 400; + const finalMessage = + status === 401 + ? 'Invalid or expired access token. Retrying' + : 'Lack of permissions to perform the operation. Aborting'; + throw new TransformerProxyError( + `LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. ${finalMessage}`, + finalStatus, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(finalStatus), + }, + destinationResponse, + getAuthErrCategoryFromStCode(destinationResponse), + responseWithIndividualEvents, + ); + } + // if the status is 422, we need to parse the error message and construct the response array + if (status === 422) { + const destPartialStatus = constructPartialStatus(response?.message); + // if the error message is not in the expected format, we will abort all of the events + if (!destPartialStatus || lodash.isEmpty(destPartialStatus)) { + throw new TransformerProxyError( + `LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. Error parsing error message`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + getAuthErrCategoryFromStCode(status), + responseWithIndividualEvents, + ); + } + responseWithIndividualEvents = [...createResponseArray(rudderJobMetadata, destPartialStatus)]; + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; + } + throw new TransformerProxyError( + `LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. ${errorMessage}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + getAuthErrCategoryFromStCode(status), + responseWithIndividualEvents, + ); + } + + // otherwise all events are successful + responseWithIndividualEvents = rudderJobMetadata.map((metadata) => ({ + statusCode: 200, + metadata, + error: 'success', + })); + + return { + status, + message, + destinationResponse, + response: responseWithIndividualEvents, + }; +}; + +function networkHandler() { + this.prepareProxy = prepareProxyRequest; + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.responseHandler = responseHandler; +} + +module.exports = { networkHandler }; diff --git a/test/__tests__/eventValidation.test.js b/test/__tests__/eventValidation.test.js index eb58879eb7..b802b6f886 100644 --- a/test/__tests__/eventValidation.test.js +++ b/test/__tests__/eventValidation.test.js @@ -73,6 +73,97 @@ const trackingPlan = { create_time: "2021-12-14T19:19:13.666Z", update_time: "2021-12-15T13:29:59.272Z" }; + +const newTrackingPlan = { + name: "Demo Tracking Plan", + version: 1, + events: [ + { + id: "ev_22HzyIUtuhfoI80iDfAgf47GHpw", + name: "Product clicked new", + eventType: "track", + description: "Fired when an product is clicked.", + rules: { + type: "object", + $schema: "http://json-schema.org/draft-07/schema#", + required: ["properties"], + properties: { + properties: { + $schema: "http://json-schema.org/draft-07/schema#", + additionalProperties: false, + properties: { + email: { + type: ["string"] + }, + name: { + type: ["string"] + }, + prop_float: { + type: ["number"] + }, + prop_integer: { + type: ["number"] + }, + revenue: { + type: ["number"] + } + }, + type: "object", + required: [ + "email", + "name", + "prop_float", + "prop_integer", + "revenue" + ], + allOf: [ + { + properties: { + prop_integer: { + const: 2 + }, + prop_float: { + const: 2.3 + } + } + } + ] + } + } + } + }, + { + id: "ev_22HzyIUtuhfoI80iDfAgf47GHpx", + name: "", + eventType: "group", + rules: { + type: "object", + $schema: "http://json-schema.org/draft-07/schema#", + required: ["traits"], + properties: { + traits: { + additionalProperties: false, + properties: { + company: { + type: ["string"] + }, + org: { + type: ["string"] + }, + }, + type: "object", + required: [ + "company", + ] + } + } + } + } + ], + workspaceId: "dummy_workspace_id", + createdAt: "2021-12-14T19:19:13.666Z", + updatedAt: "2021-12-15T13:29:59.272Z" +}; const sourceTpConfig = { track: { allowUnplannedEvents: "true", @@ -1364,6 +1455,134 @@ const eventValidationTestCases = [ } ]; +const eventValidationWithNewPlanTestCases = [ + { + testCase: "Group is part of new Tracking Plan + additional property violation", + event: { + metadata: { + trackingPlanId: "dummy_tracking_plan_id_new", + trackingPlanVersion: "dummy_version_new", + workspaceId: "dummy_workspace_id", + mergedTpConfig, + sourceTpConfig + }, + message: { + type: "group", + userId: "user12345", + groupId: "group1", + traits: { + company: "Company", + employees: 123 + }, + context: { + traits: { + trait1: "new-val" + }, + ip: "14.5.67.21", + library: { + name: "http" + } + }, + timestamp: "2020-01-21T00:21:34.208Z" + } + }, + trackingPlan: newTrackingPlan, + output: { + dropEvent: true, + violationType: violationTypes.AdditionalProperties + } + }, + { + testCase: + "Compatibility for Spread sheet plugin + Track is not part of new Tracking Plan and allowUnplannedEvents is set to text TRUE", + event: { + metadata: { + trackingPlanId: "dummy_tracking_plan_id_new", + trackingPlanVersion: "dummy_version_new", + workspaceId: "dummy_workspace_id", + mergedTpConfig: { + allowUnplannedEvents: "TRUE", + ajvOptions: {} + }, + sourceTpConfig: { + track: { + allowUnplannedEvents: "TRUE", + ajvOptions: {} + }, + global: { + allowUnplannedEvents: "FALSE", + ajvOptions: {} + } + } + }, + message: { + type: "track", + userId: "user-demo", + event: "New Product clicked", + properties: { + name: "Rubik's Cube", + revenue: 4.99, + prop_integer: 2, + prop_float: 2.3, + email: "demo@rudderstack.com" + }, + context: { + ip: "14.5.67.21" + }, + timestamp: "2020-02-02T00:23:09.544Z" + } + }, + trackingPlan: newTrackingPlan, + output: { + dropEvent: false, + violationType: "None" + } + }, + { + testCase: + "Track is part of new Tracking Plan + no track config and unplannedProperties is set to drop", + event: { + metadata: { + trackingPlanId: "dummy_tracking_plan_id_new", + trackingPlanVersion: "dummy_version_new", + workspaceId: "dummy_workspace_id", + mergedTpConfig: { + unplannedProperties: "drop", + ajvOptions: {} + }, + sourceTpConfig: { + global: { + unplannedProperties: "drop", + ajvOptions: {} + } + } + }, + message: { + type: "track", + userId: "user-demo", + event: "Product clicked new", + properties: { + name: "Rubik's Cube", + revenue: 4.99, + prop_integer: 2, + prop_float: 2.3, + email: "demo@rudderstack.com", + mobile: "999888777666" + }, + context: { + ip: "14.5.67.21" + }, + timestamp: "2020-02-02T00:23:09.544Z" + } + }, + trackingPlan: newTrackingPlan, + output: { + dropEvent: true, + violationType: violationTypes.AdditionalProperties + } + }, +]; + describe("Supported Event types testing", () => { eventTypesTestCases.forEach(testCase => { it(`should return isSupportedOrNot ${testCase.output} for this input eventType ${testCase.eventType} everytime`, () => { @@ -1389,6 +1608,22 @@ describe("Handle validation", () => { }); }); +describe("Handle validation with new tracking plan payload", () => { + eventValidationWithNewPlanTestCases.forEach(testCase => { + it(`should return dropEvent: ${testCase.output.dropEvent}, violationType: ${testCase.output.violationType}`, async () => { + fetch.mockResolvedValue({ + json: jest.fn().mockResolvedValue(testCase.trackingPlan), + status: 200 + }); + const { dropEvent, violationType } = await handleValidation( + testCase.event + ); + expect(dropEvent).toEqual(testCase.output.dropEvent); + expect(violationType).toEqual(testCase.output.violationType); + }); + }); +}); + describe("HandleValidationErrors", () => { validationErrorsTestCases.forEach(testCase => { it(`should return dropEvent ${testCase.output} for ${testCase.test}`, () => { diff --git a/test/__tests__/pinterestConversion-cdk.test.ts b/test/__tests__/pinterestConversion-cdk.test.ts index f4da92eea9..6aaa710ed7 100644 --- a/test/__tests__/pinterestConversion-cdk.test.ts +++ b/test/__tests__/pinterestConversion-cdk.test.ts @@ -1,6 +1,7 @@ +import { structuredLogger as logger } from '@rudderstack/integrations-lib'; import fs from 'fs'; import path from 'path'; -import { processCdkV2Workflow, getWorkflowEngine, executeWorkflow } from '../../src/cdk/v2/handler'; +import { executeWorkflow, getWorkflowEngine, processCdkV2Workflow } from '../../src/cdk/v2/handler'; import tags from '../../src/v0/util/tags'; const integration = 'pinterest_tag'; @@ -22,7 +23,12 @@ describe(`${name} Tests`, () => { it(`${name} - payload: ${index}`, async () => { const expected = expectedData[index]; try { - const output = await processCdkV2Workflow(integration, input, tags.FEATURES.PROCESSOR); + const output = await processCdkV2Workflow( + integration, + input, + tags.FEATURES.PROCESSOR, + logger, + ); expect(output).toEqual(expected); } catch (error: any) { expect(error.message).toEqual(expected.error); @@ -46,7 +52,12 @@ describe(`${name} Tests`, () => { it(`${name} - payload: ${index}`, async () => { const expected = expectedData[index]; try { - const output = await processCdkV2Workflow(integration, input, tags.FEATURES.PROCESSOR); + const output = await processCdkV2Workflow( + integration, + input, + tags.FEATURES.PROCESSOR, + logger, + ); expect(output).toEqual(expected); } catch (error: any) { expect(error.message).toEqual(expected.error); @@ -91,6 +102,7 @@ describe(`${name} Tests`, () => { integration, inputRouterErrorData, tags.FEATURES.ROUTER, + logger, ); expect(output).toEqual(expectedRouterErrorData); }); @@ -98,7 +110,12 @@ describe(`${name} Tests`, () => { describe('Default Batch size', () => { inputRouterData.forEach((input, index) => { it(`Payload: ${index}`, async () => { - const output = await processCdkV2Workflow(integration, input, tags.FEATURES.ROUTER); + const output = await processCdkV2Workflow( + integration, + input, + tags.FEATURES.ROUTER, + logger, + ); expect(output).toEqual(expectedRouterData[index]); }); }); diff --git a/test/__tests__/user_transformation.test.js b/test/__tests__/user_transformation.test.js index 8b781cda9a..924bf4f791 100644 --- a/test/__tests__/user_transformation.test.js +++ b/test/__tests__/user_transformation.test.js @@ -37,6 +37,10 @@ const { parserForImport } = require("../../src/util/parser"); const { RetryRequestError, RespStatusError } = require("../../src/util/utils"); const OPENFAAS_GATEWAY_URL = "http://localhost:8080"; +const defaultBasicAuth = { + "username": "", + "password": "" +}; const randomID = () => Math.random() @@ -1400,12 +1404,14 @@ describe("Python transformations", () => { expect(axios.post).toHaveBeenCalledTimes(1); expect(axios.post).toHaveBeenCalledWith( `${OPENFAAS_GATEWAY_URL}/system/functions`, - expect.objectContaining({ name: funcName, service: funcName }) + expect.objectContaining({ name: funcName, service: funcName }), + { auth: defaultBasicAuth }, ); expect(axios.get).toHaveBeenCalledTimes(1); expect(axios.get).toHaveBeenCalledWith( `${OPENFAAS_GATEWAY_URL}/function/${funcName}`, - {"headers": {"X-REQUEST-TYPE": "HEALTH-CHECK"}} + {"headers": {"X-REQUEST-TYPE": "HEALTH-CHECK"}}, + { auth: defaultBasicAuth }, ); }); @@ -1622,7 +1628,8 @@ describe("Python transformations", () => { expect(axios.post).toHaveBeenCalledTimes(1); expect(axios.post).toHaveBeenCalledWith( `${OPENFAAS_GATEWAY_URL}/function/${funcName}`, - inputData + inputData, + { auth: defaultBasicAuth }, ); }); @@ -1655,17 +1662,20 @@ describe("Python transformations", () => { expect(axios.post).toHaveBeenCalledTimes(2); expect(axios.post).toHaveBeenCalledWith( `${OPENFAAS_GATEWAY_URL}/function/${funcName}`, - inputData + inputData, + { auth: defaultBasicAuth }, ); expect(axios.post).toHaveBeenCalledWith( `${OPENFAAS_GATEWAY_URL}/system/functions`, - expect.objectContaining({ name: funcName, service: funcName }) + expect.objectContaining({ name: funcName, service: funcName }), + { auth: defaultBasicAuth }, ); expect(axios.get).toHaveBeenCalledTimes(1); expect(axios.get).toHaveBeenCalledWith( `${OPENFAAS_GATEWAY_URL}/function/${funcName}`, - {"headers": {"X-REQUEST-TYPE": "HEALTH-CHECK"}} + {"headers": {"X-REQUEST-TYPE": "HEALTH-CHECK"}}, + { auth: defaultBasicAuth }, ); }); diff --git a/test/apitests/service.api.test.ts b/test/apitests/service.api.test.ts index 266619b6ac..e46357f824 100644 --- a/test/apitests/service.api.test.ts +++ b/test/apitests/service.api.test.ts @@ -1,13 +1,13 @@ import fs from 'fs'; -import path from 'path'; -import request from 'supertest'; import { createHttpTerminator } from 'http-terminator'; import Koa from 'koa'; import bodyParser from 'koa-bodyparser'; +import path from 'path'; import setValue from 'set-value'; -import { applicationRoutes } from '../../src/routes'; -import { FetchHandler } from '../../src/helpers/fetchHandlers'; +import request from 'supertest'; import networkHandlerFactory from '../../src/adapters/networkHandlerFactory'; +import { FetchHandler } from '../../src/helpers/fetchHandlers'; +import { applicationRoutes } from '../../src/routes'; let server: any; const OLD_ENV = process.env; diff --git a/test/integrations/destinations/awin/data.ts b/test/integrations/destinations/awin/data.ts index 64c8fbc2a1..8ca294b5fb 100644 --- a/test/integrations/destinations/awin/data.ts +++ b/test/integrations/destinations/awin/data.ts @@ -843,4 +843,319 @@ export const data = [ }, }, }, + { + name: 'awin', + description: 'Track call- with product array', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + advertiserId: '1234', + eventsToTrack: [ + { + eventName: 'abc', + }, + { + eventName: 'prop2', + }, + { + eventName: 'prop3', + }, + ], + }, + }, + message: { + type: 'track', + event: 'prop2', + sentAt: '2022-01-20T13:39:21.033Z', + userId: 'user123456001', + channel: 'web', + properties: { + currency: 'INR', + voucherCode: '1bcu1', + amount: 500, + commissionGroup: 'sales', + cks: 'new', + testMode: '1', + order_id: 'QW123', + products: [ + { + product_id: '123', + name: 'Product 1', + price: 10, + quantity: 1, + sku: undefined, + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: undefined, + }, + ], + }, + context: { + os: { + name: '', + version: '', + }, + app: { + name: 'RudderLabs JavaScript SDK', + build: '1.0.0', + version: '1.2.20', + namespace: 'com.rudderlabs.javascript', + }, + page: { + url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + path: '/Testing/App_for_LaunchDarkly/ourSdk.html', + title: 'Document', + search: '', + tab_url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + referrer: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/', + initial_referrer: '$direct', + referring_domain: '127.0.0.1:7307', + initial_referring_domain: '', + }, + locale: 'en-US', + screen: { + width: 1440, + height: 900, + density: 2, + innerWidth: 536, + innerHeight: 689, + }, + traits: { + city: 'Pune', + name: 'First User', + email: 'firstUser@testmail.com', + title: 'VP', + gender: 'female', + avatar: 'https://i.pravatar.cc/300', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.2.20', + }, + campaign: {}, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36', + }, + rudderId: '553b5522-c575-40a7-8072-9741c5f9a647', + messageId: '831f1fa5-de84-4f22-880a-4c3f23fc3f04', + anonymousId: 'bf412108-0357-4330-b119-7305e767823c', + integrations: { + All: true, + }, + originalTimestamp: '2022-01-20T13:39:21.032Z', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.awin1.com/sread.php', + headers: {}, + params: { + amount: 500, + ch: 'aw', + parts: 'sales:500', + cr: 'INR', + tt: 'ss', + tv: '2', + vc: '1bcu1', + cks: 'new', + merchant: '1234', + testmode: '1', + ref: 'QW123', + 'bd[0]': 'AW:P|1234|QW123|123|Product%201|10|1||sales%3A500|Category%201', + 'bd[1]': 'AW:P|1234|QW123|456|Product%202|20|2|SKU456|sales%3A500|', + }, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'awin', + description: 'Track call- with product array where important keys might be missing.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + advertiserId: '1234', + eventsToTrack: [ + { + eventName: 'abc', + }, + { + eventName: 'prop2', + }, + { + eventName: 'prop3', + }, + ], + }, + }, + message: { + type: 'track', + event: 'prop2', + sentAt: '2022-01-20T13:39:21.033Z', + userId: 'user123456001', + channel: 'web', + properties: { + currency: 'INR', + voucherCode: '1bcu1', + amount: 500, + commissionGroup: 'sales', + cks: 'new', + testMode: '1', + order_id: 'QW123', + products: [ + { + price: 10, + quantity: 1, + sku: undefined, + category: 'Category 1', + }, + { + product_id: '456', + name: 'Product 2', + price: 20, + quantity: 2, + sku: 'SKU456', + category: undefined, + }, + ], + }, + context: { + os: { + name: '', + version: '', + }, + app: { + name: 'RudderLabs JavaScript SDK', + build: '1.0.0', + version: '1.2.20', + namespace: 'com.rudderlabs.javascript', + }, + page: { + url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + path: '/Testing/App_for_LaunchDarkly/ourSdk.html', + title: 'Document', + search: '', + tab_url: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/ourSdk.html', + referrer: 'http://127.0.0.1:7307/Testing/App_for_LaunchDarkly/', + initial_referrer: '$direct', + referring_domain: '127.0.0.1:7307', + initial_referring_domain: '', + }, + locale: 'en-US', + screen: { + width: 1440, + height: 900, + density: 2, + innerWidth: 536, + innerHeight: 689, + }, + traits: { + city: 'Pune', + name: 'First User', + email: 'firstUser@testmail.com', + title: 'VP', + gender: 'female', + avatar: 'https://i.pravatar.cc/300', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.2.20', + }, + campaign: {}, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36', + }, + rudderId: '553b5522-c575-40a7-8072-9741c5f9a647', + messageId: '831f1fa5-de84-4f22-880a-4c3f23fc3f04', + anonymousId: 'bf412108-0357-4330-b119-7305e767823c', + integrations: { + All: true, + }, + originalTimestamp: '2022-01-20T13:39:21.032Z', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.awin1.com/sread.php', + headers: {}, + params: { + amount: 500, + ch: 'aw', + parts: 'sales:500', + cr: 'INR', + tt: 'ss', + tv: '2', + vc: '1bcu1', + cks: 'new', + merchant: '1234', + testmode: '1', + ref: 'QW123', + 'bd[0]': 'AW:P|1234|QW123|456|Product%202|20|2|SKU456|sales%3A500|', + }, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/bloomreach/common.ts b/test/integrations/destinations/bloomreach/common.ts new file mode 100644 index 0000000000..798e744cbc --- /dev/null +++ b/test/integrations/destinations/bloomreach/common.ts @@ -0,0 +1,99 @@ +import { Destination } from '../../../../src/types'; + +const destType = 'bloomreach'; +const destTypeInUpperCase = 'BLOOMREACH'; +const displayName = 'bloomreach'; +const channel = 'web'; +const destination: Destination = { + Config: { + apiBaseUrl: 'https://demoapp-api.bloomreach.com', + apiKey: 'test-api-key', + apiSecret: 'test-api-secret', + projectToken: 'test-project-token', + hardID: 'registered', + softID: 'cookie', + }, + DestinationDefinition: { + DisplayName: displayName, + ID: '123', + Name: destTypeInUpperCase, + Config: { cdkV2Enabled: true }, + }, + Enabled: true, + ID: '123', + Name: destTypeInUpperCase, + Transformations: [], + WorkspaceID: 'test-workspace-id', +}; + +const traits = { + email: 'test@example.com', + firstName: 'John', + lastName: 'Doe', + phone: '1234567890', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, +}; + +const properties = { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + category: 'Games', + name: 'Cones of Dunshire', + brand: 'Wyatt Games', + variant: 'expansion pack', + price: 49.99, + quantity: 5, + coupon: 'PREORDER15', + currency: 'USD', + position: 1, + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.webp', + key1: 'value1', +}; +const endpoint = 'https://demoapp-api.bloomreach.com/track/v2/projects/test-project-token/batch'; + +const processorInstrumentationErrorStatTags = { + destType: destTypeInUpperCase, + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', +}; + +const RouterInstrumentationErrorStatTags = { + ...processorInstrumentationErrorStatTags, + feature: 'router', +}; + +const proxyV1RetryableErrorStatTags = { + ...RouterInstrumentationErrorStatTags, + errorCategory: 'network', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', +}; + +const headers = { + 'Content-Type': 'application/json', + Authorization: 'Basic dGVzdC1hcGkta2V5OnRlc3QtYXBpLXNlY3JldA==', +}; + +export { + destType, + channel, + destination, + processorInstrumentationErrorStatTags, + RouterInstrumentationErrorStatTags, + traits, + headers, + properties, + endpoint, + proxyV1RetryableErrorStatTags, +}; diff --git a/test/integrations/destinations/bloomreach/dataDelivery/business.ts b/test/integrations/destinations/bloomreach/dataDelivery/business.ts new file mode 100644 index 0000000000..9e71b7a2fd --- /dev/null +++ b/test/integrations/destinations/bloomreach/dataDelivery/business.ts @@ -0,0 +1,195 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; +import { destType, headers, properties, endpoint } from '../common'; + +const customerProperties = { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + city: 'New York', + country: 'USA', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, +}; + +const metadataArray = [generateMetadata(1), generateMetadata(2)]; + +// https://documentation.bloomreach.com/engagement/reference/tips-and-best-practices +export const businessProxyV1: ProxyV1TestData[] = [ + { + id: 'bloomreach_v1_business_scenario_1', + name: destType, + description: + '[Proxy v1 API] :: Test for a valid request - where the destination responds with 200 with error for request 2 in a batch', + successCriteria: 'Should return 200 with partial failures within the response payload', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params: {}, + JSON: { + commands: [ + { + name: 'customers', + data: { + customer_ids: { + cookie: '97c46c81-3140-456d-b2a9-690d70aaca35', + }, + update_timestamp: 1709405952, + properties: customerProperties, + }, + }, + { + name: 'customers', + data: { + customer_ids: {}, + }, + }, + ], + }, + endpoint, + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[BLOOMREACH Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + results: [ + { + success: true, + }, + { + success: false, + errors: ['At least one id should be specified.'], + }, + ], + start_time: 1710771351.9885373, + end_time: 1710771351.9891083, + success: true, + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 400, + metadata: generateMetadata(2), + error: 'At least one id should be specified.', + }, + ], + }, + }, + }, + }, + }, + { + id: 'bloomreach_v1_business_scenario_2', + name: destType, + description: + '[Proxy v1 API] :: Test for a valid request - where the destination responds with 200 without any error', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + headers, + params: {}, + JSON: { + commands: [ + { + name: 'customers/events', + data: { + customer_ids: { + cookie: '97c46c81-3140-456d-b2a9-690d70aaca35', + }, + timestamp: 1709566376, + properties, + event_type: 'test_event', + }, + }, + { + name: 'customers', + data: { + customer_ids: { + cookie: '97c46c81-3140-456d-b2a9-690d70aaca35', + }, + update_timestamp: 1709405952, + properties: customerProperties, + }, + }, + ], + }, + endpoint, + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[BLOOMREACH Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + results: [ + { + success: true, + }, + { + success: true, + }, + ], + start_time: 1710771351.9885373, + end_time: 1710771351.9891083, + success: true, + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + { + statusCode: 200, + metadata: generateMetadata(2), + error: 'success', + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/dataDelivery/data.ts b/test/integrations/destinations/bloomreach/dataDelivery/data.ts new file mode 100644 index 0000000000..5099eafce7 --- /dev/null +++ b/test/integrations/destinations/bloomreach/dataDelivery/data.ts @@ -0,0 +1,3 @@ +import { businessProxyV1 } from './business'; +import { otherProxyV1 } from './other'; +export const data = [...businessProxyV1, ...otherProxyV1]; diff --git a/test/integrations/destinations/bloomreach/dataDelivery/other.ts b/test/integrations/destinations/bloomreach/dataDelivery/other.ts new file mode 100644 index 0000000000..f0dd9cc09a --- /dev/null +++ b/test/integrations/destinations/bloomreach/dataDelivery/other.ts @@ -0,0 +1,212 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload, generateMetadata } from '../../../testUtils'; +import { destType, proxyV1RetryableErrorStatTags } from '../common'; + +const metadataArray = [generateMetadata(1)]; + +// https://documentation.bloomreach.com/engagement/reference/tips-and-best-practices +export const otherProxyV1: ProxyV1TestData[] = [ + { + id: 'bloomreach_v1_other_scenario_1', + name: destType, + description: + '[Proxy v1 API] :: Scenario for testing Service Unavailable error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_service_not_available', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"error":{"message":"Service Unavailable","description":"The server is currently unable to handle the request due to temporary overloading or maintenance of the server. Please try again later."}}', + statusCode: 503, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH: Error encountered in transformer proxy V1', + status: 503, + }, + }, + }, + }, + }, + { + id: 'bloomreach_v1_other_scenario_2', + name: destType, + description: '[Proxy v1 API] :: Scenario for testing Internal Server error from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_internal_server_error', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Internal Server Error"', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH: Error encountered in transformer proxy V1', + status: 500, + }, + }, + }, + }, + }, + { + id: 'bloomreach_v1_other_scenario_3', + name: destType, + description: '[Proxy v1 API] :: Scenario for testing Gateway Time Out error from destination', + successCriteria: 'Should return 504 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_gateway_time_out', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '"Gateway Timeout"', + statusCode: 504, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH: Error encountered in transformer proxy V1', + status: 504, + }, + }, + }, + }, + }, + { + id: 'bloomreach_v1_other_scenario_4', + name: destType, + description: '[Proxy v1 API] :: Scenario for testing null response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_null_response', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH: Error encountered in transformer proxy V1', + status: 500, + }, + }, + }, + }, + }, + { + id: 'bloomreach_v1_other_scenario_5', + name: destType, + description: + '[Proxy v1 API] :: Scenario for testing null and no status response from destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Framework', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + endpoint: 'https://random_test_url/test_for_null_and_no_status', + }, + metadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: '""', + statusCode: 500, + metadata: generateMetadata(1), + }, + ], + statTags: proxyV1RetryableErrorStatTags, + message: 'BLOOMREACH: Error encountered in transformer proxy V1', + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/mocks.ts b/test/integrations/destinations/bloomreach/mocks.ts new file mode 100644 index 0000000000..ba3b22b52a --- /dev/null +++ b/test/integrations/destinations/bloomreach/mocks.ts @@ -0,0 +1,5 @@ +import * as config from '../../../../src/cdk/v2/destinations/bloomreach/config'; + +export const defaultMockFns = () => { + jest.replaceProperty(config, 'MAX_BATCH_SIZE', 3 as typeof config.MAX_BATCH_SIZE); +}; diff --git a/test/integrations/destinations/bloomreach/network.ts b/test/integrations/destinations/bloomreach/network.ts new file mode 100644 index 0000000000..b20ff881b8 --- /dev/null +++ b/test/integrations/destinations/bloomreach/network.ts @@ -0,0 +1,124 @@ +import { destType, headers, properties, endpoint } from './common'; + +export const networkCallsData = [ + { + httpReq: { + url: endpoint, + data: { + commands: [ + { + name: 'customers', + data: { + customer_ids: { + cookie: '97c46c81-3140-456d-b2a9-690d70aaca35', + }, + update_timestamp: 1709405952, + properties: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + city: 'New York', + country: 'USA', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, + }, + }, + }, + { + name: 'customers', + data: { + customer_ids: {}, + }, + }, + ], + }, + params: { destination: destType }, + headers, + method: 'POST', + }, + httpRes: { + data: { + results: [ + { + success: true, + }, + { + success: false, + errors: ['At least one id should be specified.'], + }, + ], + start_time: 1710771351.9885373, + end_time: 1710771351.9891083, + success: true, + }, + status: 200, + statusText: 'Ok', + }, + }, + { + httpReq: { + url: endpoint, + data: { + commands: [ + { + name: 'customers/events', + data: { + customer_ids: { + cookie: '97c46c81-3140-456d-b2a9-690d70aaca35', + }, + timestamp: 1709566376, + properties, + event_type: 'test_event', + }, + }, + { + name: 'customers', + data: { + customer_ids: { + cookie: '97c46c81-3140-456d-b2a9-690d70aaca35', + }, + update_timestamp: 1709405952, + properties: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + city: 'New York', + country: 'USA', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, + }, + }, + }, + ], + }, + params: { destination: destType }, + headers, + method: 'POST', + }, + httpRes: { + data: { + results: [ + { + success: true, + }, + { + success: true, + }, + ], + start_time: 1710771351.9885373, + end_time: 1710771351.9891083, + success: true, + }, + status: 200, + statusText: 'Ok', + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/processor/data.ts b/test/integrations/destinations/bloomreach/processor/data.ts new file mode 100644 index 0000000000..a3633ad0dd --- /dev/null +++ b/test/integrations/destinations/bloomreach/processor/data.ts @@ -0,0 +1,5 @@ +import { validation } from './validation'; +import { identify } from './identify'; +import { track } from './track'; +import { page } from './page'; +export const data = [...identify, ...track, ...page, ...validation]; diff --git a/test/integrations/destinations/bloomreach/processor/identify.ts b/test/integrations/destinations/bloomreach/processor/identify.ts new file mode 100644 index 0000000000..2a79cb57e3 --- /dev/null +++ b/test/integrations/destinations/bloomreach/processor/identify.ts @@ -0,0 +1,156 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, transformResultBuilder } from '../../../testUtils'; +import { destType, destination, traits, headers, endpoint } from '../common'; + +export const identify: ProcessorTestData[] = [ + { + id: 'bloomreach-identify-test-1', + name: destType, + description: 'Identify call to create/update customer properties', + scenario: 'Framework+Business', + successCriteria: 'Response should contain all the mapping and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + userId: 'userId123', + anonymousId: 'anonId123', + traits, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint, + headers, + JSON: { + data: { + customer_ids: { registered: 'userId123', cookie: 'anonId123' }, + properties: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + city: 'New York', + country: 'USA', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, + }, + update_timestamp: 1709566376, + }, + name: 'customers', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'bloomreach-identify-test-2', + name: destType, + description: 'Identify call with multiple hard and soft identifiers using integration object', + scenario: 'Framework+Business', + successCriteria: + 'Response should contain multiple hard and soft identifiers and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + userId: 'userId123', + anonymousId: 'anonId123', + traits, + integrations: { + All: true, + bloomreach: { + hardID: { + hardID1: 'value1', + }, + softID: { + google_analytics: 'gaId123', + softID2: 'value2', + }, + }, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint, + headers, + JSON: { + data: { + customer_ids: { + registered: 'userId123', + cookie: 'anonId123', + hardID1: 'value1', + google_analytics: 'gaId123', + softID2: 'value2', + }, + properties: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + city: 'New York', + country: 'USA', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, + }, + update_timestamp: 1709566376, + }, + name: 'customers', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/processor/page.ts b/test/integrations/destinations/bloomreach/processor/page.ts new file mode 100644 index 0000000000..0c2d27989d --- /dev/null +++ b/test/integrations/destinations/bloomreach/processor/page.ts @@ -0,0 +1,72 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, transformResultBuilder } from '../../../testUtils'; +import { destType, destination, headers, endpoint } from '../common'; + +const properties = { + category: 'Docs', + path: '', + referrer: '', + search: '', + title: '', + url: '', +}; + +export const page: ProcessorTestData[] = [ + { + id: 'bloomreach-page-test-1', + name: destType, + description: 'Page call with category, name', + scenario: 'Framework+Business', + successCriteria: + 'Response should contain event_name = "Viewed {{ category }} {{ name }} Page" and properties and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'page', + anonymousId: 'anonId123', + name: 'Integration', + properties, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint, + headers, + JSON: { + data: { + customer_ids: { cookie: 'anonId123' }, + properties, + timestamp: 1709566376, + event_type: 'Viewed Docs Integration Page', + }, + name: 'customers/events', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/processor/track.ts b/test/integrations/destinations/bloomreach/processor/track.ts new file mode 100644 index 0000000000..a369f508b2 --- /dev/null +++ b/test/integrations/destinations/bloomreach/processor/track.ts @@ -0,0 +1,173 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata, transformResultBuilder } from '../../../testUtils'; +import { destType, destination, headers, properties, endpoint } from '../common'; + +export const track: ProcessorTestData[] = [ + { + id: 'bloomreach-track-test-1', + name: destType, + description: 'Track call with anonymous user', + scenario: 'Framework+Business', + successCriteria: 'Response should contain all the mapping and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + anonymousId: 'anonId123', + event: 'Product Viewed', + properties, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint, + headers, + JSON: { + data: { + customer_ids: { cookie: 'anonId123' }, + properties, + timestamp: 1709566376, + event_type: 'Product Viewed', + }, + name: 'customers/events', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'bloomreach-track-test-2', + name: destType, + description: 'Track call with known user', + scenario: 'Framework+Business', + successCriteria: 'Response should contain all the mapping and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + userId: 'userId123', + anonymousId: 'anonId123', + event: 'Product Added', + properties, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint, + headers, + JSON: { + data: { + customer_ids: { registered: 'userId123', cookie: 'anonId123' }, + properties, + timestamp: 1709566376, + event_type: 'Product Added', + }, + name: 'customers/events', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'bloomreach-track-test-3', + name: destType, + description: 'Track call with no properties', + scenario: 'Framework+Business', + successCriteria: 'Response should contain all the mapping and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + anonymousId: 'anonId123', + event: 'test_event', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + method: 'POST', + userId: '', + endpoint, + headers, + JSON: { + data: { + customer_ids: { cookie: 'anonId123' }, + timestamp: 1709566376, + event_type: 'test_event', + }, + name: 'customers/events', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/processor/validation.ts b/test/integrations/destinations/bloomreach/processor/validation.ts new file mode 100644 index 0000000000..ff959d74c6 --- /dev/null +++ b/test/integrations/destinations/bloomreach/processor/validation.ts @@ -0,0 +1,131 @@ +import { ProcessorTestData } from '../../../testTypes'; +import { generateMetadata } from '../../../testUtils'; +import { destType, destination, processorInstrumentationErrorStatTags } from '../common'; + +export const validation: ProcessorTestData[] = [ + { + id: 'bloomreach-validation-test-1', + name: destType, + description: 'Missing userId and anonymousId', + scenario: 'Framework', + successCriteria: 'Instrumentation Error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Either one of userId or anonymousId is required. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Either one of userId or anonymousId is required. Aborting', + metadata: generateMetadata(1), + statTags: processorInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'bloomreach-validation-test-2', + name: destType, + description: 'Unsupported message type -> group', + scenario: 'Framework', + successCriteria: 'Instrumentation Error for Unsupported message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'group', + userId: 'userId123', + channel: 'mobile', + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'message type group is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: message type group is not supported', + metadata: generateMetadata(1), + statTags: processorInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'bloomreach-validation-test-3', + name: destType, + description: 'Missing required field -> timestamp', + scenario: 'Framework', + successCriteria: 'Instrumentation Error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + integrations: { + All: true, + }, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Timestamp is not present. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Timestamp is not present. Aborting', + metadata: generateMetadata(1), + statTags: processorInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/bloomreach/router/data.ts b/test/integrations/destinations/bloomreach/router/data.ts new file mode 100644 index 0000000000..e99d0cc8cd --- /dev/null +++ b/test/integrations/destinations/bloomreach/router/data.ts @@ -0,0 +1,220 @@ +import { generateMetadata } from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; +import { + destType, + destination, + traits, + properties, + headers, + endpoint, + RouterInstrumentationErrorStatTags, +} from '../common'; + +const routerRequest = { + input: [ + { + message: { + type: 'track', + anonymousId: 'anonId1', + event: 'test_event_1A', + properties, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + destination, + }, + { + message: { + type: 'identify', + anonymousId: 'anonId1', + userId: 'userId1', + traits, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(2), + destination, + }, + { + message: { + type: 'track', + anonymousId: 'anonId2', + event: 'test_event_2A', + properties, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(3), + destination, + }, + { + message: { + type: 'track', + anonymousId: 'anonId1', + userId: 'userId1', + event: 'test_event_1B', + properties, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(4), + destination, + }, + { + message: { + type: 'identify', + traits, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(5), + destination, + }, + ], + destType, +}; +export const data = [ + { + id: 'bloomreach-router-test-1', + name: destType, + description: 'Basic Router Test to test multiple payloads', + scenario: 'Framework', + successCriteria: 'All events should be transformed successfully and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint, + headers, + params: {}, + body: { + JSON: { + commands: [ + { + data: { + customer_ids: { cookie: 'anonId1' }, + properties, + timestamp: 1709566376, + event_type: 'test_event_1A', + }, + name: 'customers/events', + }, + { + data: { + customer_ids: { + registered: 'userId1', + cookie: 'anonId1', + }, + properties: { + email: 'test@example.com', + first_name: 'John', + last_name: 'Doe', + phone: '1234567890', + city: 'New York', + country: 'USA', + address: { + city: 'New York', + country: 'USA', + pinCode: '123456', + }, + }, + update_timestamp: 1709566376, + }, + name: 'customers', + }, + { + data: { + customer_ids: { cookie: 'anonId2' }, + properties, + timestamp: 1709566376, + event_type: 'test_event_2A', + }, + name: 'customers/events', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1), generateMetadata(2), generateMetadata(3)], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint, + headers, + params: {}, + body: { + JSON: { + commands: [ + { + data: { + customer_ids: { registered: 'userId1', cookie: 'anonId1' }, + properties, + timestamp: 1709566376, + event_type: 'test_event_1B', + }, + name: 'customers/events', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(4)], + batched: true, + statusCode: 200, + destination, + }, + { + metadata: [generateMetadata(5)], + batched: false, + statusCode: 400, + error: 'Either one of userId or anonymousId is required. Aborting', + statTags: RouterInstrumentationErrorStatTags, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/bluecore/ecommTestData.ts b/test/integrations/destinations/bluecore/ecommTestData.ts index de7584df78..19b63e7bda 100644 --- a/test/integrations/destinations/bluecore/ecommTestData.ts +++ b/test/integrations/destinations/bluecore/ecommTestData.ts @@ -73,7 +73,7 @@ const commonOutputHeaders = { 'Content-Type': 'application/json', }; -const eventEndPoint = 'https://api.bluecore.com/api/track/mobile/v1'; +const eventEndPoint = 'https://api.bluecore.app/api/track/mobile/v1'; export const ecomTestData = [ { @@ -296,7 +296,11 @@ export const ecomTestData = [ customer: { age: '22', email: 'test@rudderstack.com', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', + phone: '9112340375', }, + product_id: '123', products: [ { id: '123', @@ -304,9 +308,11 @@ export const ecomTestData = [ property2: 'value2', }, ], + property1: 'value1', + property2: 'value2', + token: 'dummy_sandbox', }, event: 'viewed_product', - token: 'dummy_sandbox', }, userId: '', }), @@ -379,8 +385,11 @@ export const ecomTestData = [ JSON: { properties: { distinct_id: 'user@1', + product_id: '123', customer: { age: '22', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', }, products: [ { @@ -389,9 +398,11 @@ export const ecomTestData = [ property2: 'value2', }, ], + property1: 'value1', + property2: 'value2', + token: 'dummy_sandbox', }, event: 'wishlist', - token: 'dummy_sandbox', }, userId: '', }), @@ -406,8 +417,11 @@ export const ecomTestData = [ JSON: { properties: { distinct_id: 'user@1', + product_id: '123', customer: { age: '22', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', }, products: [ { @@ -416,9 +430,11 @@ export const ecomTestData = [ property2: 'value2', }, ], + token: 'dummy_sandbox', + property1: 'value1', + property2: 'value2', }, event: 'add_to_cart', - token: 'dummy_sandbox', }, userId: '', }), diff --git a/test/integrations/destinations/bluecore/identifyTestData.ts b/test/integrations/destinations/bluecore/identifyTestData.ts index 660e335bc6..fee27ccf0f 100644 --- a/test/integrations/destinations/bluecore/identifyTestData.ts +++ b/test/integrations/destinations/bluecore/identifyTestData.ts @@ -55,6 +55,10 @@ const commonOutputCustomerProperties = { first_name: 'Test', last_name: 'Rudderlabs', sex: 'non-binary', + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + db: '19950715', + gender: 'non-binary', + phone: '+1234589947', address: { city: 'Kolkata', state: 'WB', @@ -71,7 +75,7 @@ const anonymousId = '97c46c81-3140-456d-b2a9-690d70aaca35'; const userId = 'user@1'; const sentAt = '2021-01-03T17:02:53.195Z'; const originalTimestamp = '2021-01-03T17:02:53.193Z'; -const commonEndpoint = 'https://api.bluecore.com/api/track/mobile/v1'; +const commonEndpoint = 'https://api.bluecore.app/api/track/mobile/v1'; export const identifyData = [ { @@ -118,8 +122,8 @@ export const identifyData = [ properties: { distinct_id: 'abc@gmail.com', customer: { ...commonOutputCustomerProperties, email: 'abc@gmail.com' }, + token: 'dummy_sandbox', }, - token: 'dummy_sandbox', event: 'customer_patch', }, }), @@ -302,8 +306,9 @@ export const identifyData = [ properties: { distinct_id: 'user@1', customer: { ...commonOutputCustomerProperties, email: 'abc@gmail.com' }, + token: 'dummy_sandbox', }, - token: 'dummy_sandbox', + event: 'identify', }, }), @@ -361,8 +366,8 @@ export const identifyData = [ properties: { distinct_id: '54321', customer: { ...commonOutputCustomerProperties, email: 'abc@gmail.com' }, + token: 'dummy_sandbox', }, - token: 'dummy_sandbox', event: 'customer_patch', }, }), diff --git a/test/integrations/destinations/bluecore/trackTestData.ts b/test/integrations/destinations/bluecore/trackTestData.ts index 72d48bf93d..7474127558 100644 --- a/test/integrations/destinations/bluecore/trackTestData.ts +++ b/test/integrations/destinations/bluecore/trackTestData.ts @@ -86,7 +86,7 @@ const commonOutputHeaders = { 'Content-Type': 'application/json', }; -const eventEndPoint = 'https://api.bluecore.com/api/track/mobile/v1'; +const eventEndPoint = 'https://api.bluecore.app/api/track/mobile/v1'; export const trackTestData = [ { @@ -140,6 +140,9 @@ export const trackTestData = [ customer: { age: '22', email: 'test@rudderstack.com', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', + phone: '9112340375', }, products: [ { @@ -155,9 +158,11 @@ export const trackTestData = [ quantity: 3, }, ], + property1: 'value1', + property2: 'value2', + token: 'dummy_sandbox', }, event: 'TestEven001', - token: 'dummy_sandbox', }, userId: '', }), @@ -216,13 +221,19 @@ export const trackTestData = [ JSON: { properties: { distinct_id: 'test@rudderstack.com', + product_id: '123', + property1: 'value1', + property2: 'value2', + token: 'dummy_sandbox', customer: { age: '22', email: 'test@rudderstack.com', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', + phone: '9112340375', }, }, event: 'TestEven001', - token: 'dummy_sandbox', }, userId: '', }), @@ -283,11 +294,17 @@ export const trackTestData = [ distinct_id: 'test@rudderstack.com', customer: { age: '22', + anonymousId: '9c6bd77ea9da3e68', email: 'test@rudderstack.com', + id: 'user@1', + phone: '9112340375', }, + product_id: '123', + property1: 'value1', + property2: 'value2', + token: 'dummy_sandbox', }, event: 'optin', - token: 'dummy_sandbox', }, userId: '', }), @@ -346,13 +363,19 @@ export const trackTestData = [ JSON: { properties: { distinct_id: 'test@rudderstack.com', + product_id: '123', + property1: 'value1', + property2: 'value2', + token: 'dummy_sandbox', customer: { age: '22', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', email: 'test@rudderstack.com', + phone: '9112340375', }, }, event: 'unsubscribe', - token: 'dummy_sandbox', }, userId: '', }), @@ -405,9 +428,12 @@ export const trackTestData = [ JSON: { properties: { distinct_id: '54321', + token: 'dummy_sandbox', customer: { age: '22', email: 'abc@gmail.com', + anonymousId: '9c6bd77ea9da3e68', + id: 'user@1', }, products: [ { @@ -423,9 +449,10 @@ export const trackTestData = [ quantity: 3, }, ], + property1: 'value1', + property2: 'value2', }, event: 'TestEven001', - token: 'dummy_sandbox', }, userId: '', }), diff --git a/test/integrations/destinations/blueshift/processor/data.ts b/test/integrations/destinations/blueshift/processor/data.ts index d489a38bd6..be23521621 100644 --- a/test/integrations/destinations/blueshift/processor/data.ts +++ b/test/integrations/destinations/blueshift/processor/data.ts @@ -739,7 +739,7 @@ export const data = [ body: [ { statusCode: 400, - error: 'Missing required value from "email"', + error: 'Missing required value from "emailOnly"', statTags: { errorCategory: 'dataValidation', errorType: 'instrumentation', diff --git a/test/integrations/destinations/delighted/network.ts b/test/integrations/destinations/delighted/network.ts index d9896a25e8..1ccc785ea3 100644 --- a/test/integrations/destinations/delighted/network.ts +++ b/test/integrations/destinations/delighted/network.ts @@ -27,4 +27,18 @@ export const networkCallsData = [ status: 200, }, }, + { + httpReq: { + url: 'https://api.delighted.com/v1/people.json', + method: 'GET', + headers: { Authorization: `Basic ZHVtbXlBcGlLZXlmb3JmYWlsdXJl` }, + params: { + email: 'test@rudderlabs.com', + }, + }, + httpRes: { + status: 429, + data: {}, + }, + }, ]; diff --git a/test/integrations/destinations/delighted/processor/data.ts b/test/integrations/destinations/delighted/processor/data.ts index 7a5ad7de9d..f35c2d8ecb 100644 --- a/test/integrations/destinations/delighted/processor/data.ts +++ b/test/integrations/destinations/delighted/processor/data.ts @@ -944,4 +944,93 @@ export const data = [ }, }, }, + { + name: 'delighted', + description: 'Too many request test', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + apiKey: 'dummyApiKeyforfailure', + channel: 'email', + delay: 0, + eventNamesSettings: [ + { + event: 'Product Reviewed', + }, + ], + }, + }, + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + ip: '0.0.0.0', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + }, + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + type: 'track', + userId: 'test@rudderlabs.com', + event: 'Product Reviewed', + properties: { + review_id: '12345', + product_id: '123', + rating: 3, + review_body: 'Average product, expected much more.', + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '{"message":"Error occurred while checking user: {}","destinationResponse":{"response":{},"status":429}}', + statTags: { + destType: 'DELIGHTED', + errorCategory: 'network', + errorType: 'throttled', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 429, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/freshmarketer/processor/data.ts b/test/integrations/destinations/freshmarketer/processor/data.ts index ed920faef0..07112d762f 100644 --- a/test/integrations/destinations/freshmarketer/processor/data.ts +++ b/test/integrations/destinations/freshmarketer/processor/data.ts @@ -375,7 +375,7 @@ export const data = [ status: 200, body: [ { - error: 'Missing required value from "email"', + error: 'Missing required value from "emailOnly"', statTags: { destType: 'FRESHMARKETER', errorCategory: 'dataValidation', @@ -964,7 +964,7 @@ export const data = [ status: 200, body: [ { - error: 'Missing required value from "email"', + error: 'Missing required value from "emailOnly"', statTags: { destType: 'FRESHMARKETER', errorCategory: 'dataValidation', diff --git a/test/integrations/destinations/freshsales/processor/data.ts b/test/integrations/destinations/freshsales/processor/data.ts index eca3b88d9d..7c0eca0926 100644 --- a/test/integrations/destinations/freshsales/processor/data.ts +++ b/test/integrations/destinations/freshsales/processor/data.ts @@ -505,7 +505,7 @@ export const data = [ status: 200, body: [ { - error: 'Missing required value from "email"', + error: 'Missing required value from "emailOnly"', statTags: { destType: 'FRESHSALES', errorCategory: 'dataValidation', @@ -1094,7 +1094,7 @@ export const data = [ status: 200, body: [ { - error: 'Missing required value from "email"', + error: 'Missing required value from "emailOnly"', statTags: { destType: 'FRESHSALES', errorCategory: 'dataValidation', diff --git a/test/integrations/destinations/ga4/processor/data.ts b/test/integrations/destinations/ga4/processor/data.ts index f96ca9e74a..4465ec9e2c 100644 --- a/test/integrations/destinations/ga4/processor/data.ts +++ b/test/integrations/destinations/ga4/processor/data.ts @@ -14900,4 +14900,313 @@ export const data = [ }, mockFns: defaultMockFns, }, + { + name: 'ga4', + description: '(gtag) send consents setting to ga4 with login event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + messageId: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be', + originalTimestamp: '2022-04-26T05:17:09Z', + anonymousId: 'ea5cfab2-3961-4d8a-8187-3d1858c99090', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + device: { + adTrackingEnabled: 'false', + advertisingId: 'T0T0T072-5e28-45a1-9eda-ce22a3e36d1a', + id: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + manufacturer: 'Google', + model: 'AOSP on IA Emulator', + name: 'generic_x86_arm', + type: 'ios', + attTrackingStatus: 3, + }, + ip: '0.0.0.0', + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + locale: 'en-US', + os: { + name: 'iOS', + version: '14.4.1', + }, + screen: { + density: 2, + }, + externalId: [ + { + type: 'ga4AppInstanceId', + id: 'dummyGA4AppInstanceId', + }, + { + type: 'ga4ClientId', + id: 'client_id', + }, + ], + 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', + }, + type: 'track', + event: 'login', + properties: { + method: 'Google', + }, + integrations: { + All: true, + GA4: { + consents: { + ad_personalization: 'GRANTED', + ad_user_data: 'GRANTED', + }, + }, + }, + sentAt: '2022-04-20T15:20:57Z', + }, + destination: { + Config: { + apiSecret: 'dummyApiSecret', + measurementId: 'G-123456', + firebaseAppId: '', + blockPageViewEvent: false, + typesOfClient: 'gtag', + extendPageViewParams: false, + sendUserId: false, + eventFilteringOption: 'disable', + blacklistedEvents: [ + { + eventName: '', + }, + ], + whitelistedEvents: [ + { + eventName: '', + }, + ], + enableServerSideIdentify: false, + sendLoginSignup: false, + generateLead: false, + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.google-analytics.com/mp/collect', + headers: { + HOST: 'www.google-analytics.com', + 'Content-Type': 'application/json', + }, + params: { + api_secret: 'dummyApiSecret', + measurement_id: 'G-123456', + }, + body: { + JSON: { + client_id: 'client_id', + consent: { + ad_personalization: 'GRANTED', + ad_user_data: 'GRANTED', + }, + timestamp_micros: 1650950229000000, + non_personalized_ads: true, + events: [ + { + name: 'login', + params: { + method: 'Google', + engagement_time_msec: 1, + }, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + name: 'ga4', + description: '(gtag) send consents setting to ga4 with login event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + messageId: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be', + originalTimestamp: '2022-04-26T05:17:09Z', + anonymousId: 'ea5cfab2-3961-4d8a-8187-3d1858c99090', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + device: { + adTrackingEnabled: 'false', + advertisingId: 'T0T0T072-5e28-45a1-9eda-ce22a3e36d1a', + id: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + manufacturer: 'Google', + model: 'AOSP on IA Emulator', + name: 'generic_x86_arm', + type: 'ios', + attTrackingStatus: 3, + }, + ip: '0.0.0.0', + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + locale: 'en-US', + os: { + name: 'iOS', + version: '14.4.1', + }, + screen: { + density: 2, + }, + externalId: [ + { + type: 'ga4AppInstanceId', + id: 'dummyGA4AppInstanceId', + }, + { + type: 'ga4ClientId', + id: 'client_id', + }, + ], + 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', + }, + type: 'track', + event: 'login', + properties: { + method: 'Google', + }, + integrations: { + All: true, + GA4: { + consents: { + ad_personalization: 'NOT_SPECIFIED', + ad_user_data: 'DENIED', + }, + }, + }, + sentAt: '2022-04-20T15:20:57Z', + }, + destination: { + Config: { + apiSecret: 'dummyApiSecret', + measurementId: 'G-123456', + firebaseAppId: '', + blockPageViewEvent: false, + typesOfClient: 'gtag', + extendPageViewParams: false, + sendUserId: false, + eventFilteringOption: 'disable', + blacklistedEvents: [ + { + eventName: '', + }, + ], + whitelistedEvents: [ + { + eventName: '', + }, + ], + enableServerSideIdentify: false, + sendLoginSignup: false, + generateLead: false, + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://www.google-analytics.com/mp/collect', + headers: { + HOST: 'www.google-analytics.com', + 'Content-Type': 'application/json', + }, + params: { + api_secret: 'dummyApiSecret', + measurement_id: 'G-123456', + }, + body: { + JSON: { + client_id: 'client_id', + consent: { + ad_user_data: 'DENIED', + }, + timestamp_micros: 1650950229000000, + non_personalized_ads: true, + events: [ + { + name: 'login', + params: { + method: 'Google', + engagement_time_msec: 1, + }, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, ]; diff --git a/test/integrations/destinations/hs/network.ts b/test/integrations/destinations/hs/network.ts index e29cc27562..3d3b8fd83f 100644 --- a/test/integrations/destinations/hs/network.ts +++ b/test/integrations/destinations/hs/network.ts @@ -460,6 +460,37 @@ export const networkCallsData = [ status: 200, }, }, + { + httpReq: { + url: 'https://api.hubapi.com/crm/v3/objects/contacts/search', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer dummy-access-token-hs-additonal-email', + }, + }, + httpRes: { + data: { + total: 1, + results: [ + { + id: '103689', + properties: { + createdate: '2022-07-15T15:25:08.975Z', + email: 'primary@email.com', + hs_object_id: '103604', + hs_additional_emails: 'abc@extraemail.com;secondary@email.com', + lastmodifieddate: '2022-07-15T15:26:49.590Z', + }, + createdAt: '2022-07-15T15:25:08.975Z', + updatedAt: '2022-07-15T15:26:49.590Z', + archived: false, + }, + ], + }, + status: 200, + }, + }, { httpReq: { url: 'https://api.hubapi.com/crm/v3/objects/contacts/search', diff --git a/test/integrations/destinations/hs/processor/data.ts b/test/integrations/destinations/hs/processor/data.ts index f45f3a719b..0867f2cb54 100644 --- a/test/integrations/destinations/hs/processor/data.ts +++ b/test/integrations/destinations/hs/processor/data.ts @@ -5373,4 +5373,57 @@ export const data = [ }, }, }, + { + name: 'hs', + description: 'if event name is anything other than string we throw error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + traits: { + email: 'testhubspot2@email.com', + firstname: 'Test Hubspot', + }, + }, + type: 'track', + originalTimestamp: '2019-10-15T09:35:31.291Z', + userId: '12345', + event: { name: 'event' }, + properties: { + user_actual_role: 'system_admin, system_user', + user_actual_id: 12345, + }, + sentAt: '2019-10-14T11:15:53.296Z', + }, + destination: destination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: 'Event is a required field and should be a string', + statTags: { + destType: 'HS', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 400, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/hs/router/data.ts b/test/integrations/destinations/hs/router/data.ts index 3a30232f9f..e1c3e04356 100644 --- a/test/integrations/destinations/hs/router/data.ts +++ b/test/integrations/destinations/hs/router/data.ts @@ -1688,4 +1688,145 @@ export const data = [ }, }, }, + { + name: 'hs', + description: 'getting duplicate records for secondary property', + feature: 'router', + module: 'destination', + version: 'v0', + scenario: 'buisness', + successCriteria: + 'should return 200 status code with contact needs to be updated and no email property', + input: { + request: { + body: { + input: [ + { + message: { + type: 'identify', + sentAt: '2024-03-19T18:46:36.348Z', + traits: { + lastname: 'Peñarete', + firstname: 'Karen', + }, + userId: 'secondary@email.com', + channel: 'sources', + context: { + externalId: [ + { + id: 'secondary@email.com', + type: 'HS-contacts', + identifierType: 'email', + }, + ], + mappedToDestination: 'true', + }, + originalTimestamp: '2024-03-19T18:46:36.348Z', + }, + metadata: { jobId: 3, userId: 'u1' }, + destination: { + Config: { + authorizationType: 'newPrivateAppApi', + accessToken: 'dummy-access-token-hs-additonal-email', + hubID: 'dummy-hubId', + apiKey: 'dummy-apikey', + apiVersion: 'newApi', + lookupField: 'email', + hubspotEvents: [], + }, + secretConfig: {}, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + name: 'Hubspot', + enabled: true, + workspaceId: '1TSN08muJTZwH8iCDmnnRt1pmLd', + deleted: false, + createdAt: '2020-12-30T08:39:32.005Z', + updatedAt: '2021-02-03T16:22:31.374Z', + destinationDefinition: { + id: '1aIXqM806xAVm92nx07YwKbRrO9', + name: 'HS', + displayName: 'Hubspot', + createdAt: '2020-04-09T09:24:31.794Z', + updatedAt: '2021-01-11T11:03:28.103Z', + }, + transformations: [], + isConnectionEnabled: true, + isProcessorEnabled: true, + }, + }, + ], + destType: 'hs', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.hubapi.com/crm/v3/objects/contacts/batch/update', + headers: { + 'Content-Type': 'application/json', + Authorization: 'Bearer dummy-access-token-hs-additonal-email', + }, + params: {}, + body: { + JSON: { + inputs: [ + { + properties: { lastname: 'Peñarete', firstname: 'Karen' }, + id: '103689', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [{ jobId: 3, userId: 'u1' }], + batched: true, + statusCode: 200, + destination: { + Config: { + authorizationType: 'newPrivateAppApi', + accessToken: 'dummy-access-token-hs-additonal-email', + hubID: 'dummy-hubId', + apiKey: 'dummy-apikey', + apiVersion: 'newApi', + lookupField: 'email', + hubspotEvents: [], + }, + secretConfig: {}, + ID: '1mMy5cqbtfuaKZv1IhVQKnBdVwe', + name: 'Hubspot', + enabled: true, + workspaceId: '1TSN08muJTZwH8iCDmnnRt1pmLd', + deleted: false, + createdAt: '2020-12-30T08:39:32.005Z', + updatedAt: '2021-02-03T16:22:31.374Z', + destinationDefinition: { + id: '1aIXqM806xAVm92nx07YwKbRrO9', + name: 'HS', + displayName: 'Hubspot', + createdAt: '2020-04-09T09:24:31.794Z', + updatedAt: '2021-01-11T11:03:28.103Z', + }, + transformations: [], + isConnectionEnabled: true, + isProcessorEnabled: true, + }, + }, + ], + }, + }, + }, + }, ]; diff --git a/test/integrations/destinations/impact/processor/data.ts b/test/integrations/destinations/impact/processor/data.ts index e467956d62..1e4e91e7ad 100644 --- a/test/integrations/destinations/impact/processor/data.ts +++ b/test/integrations/destinations/impact/processor/data.ts @@ -891,6 +891,7 @@ export const data = [ price: 332, quantity: 1, sku: 'G-32', + customRSProductField: 'customRSVal', }, ], }, @@ -941,6 +942,10 @@ export const data = [ from: 'variant', to: 'ItemCategory', }, + { + from: 'customRSProductField', + to: 'customImpactProductField', + }, ], enableIdentifyEvents: false, enablePageEvents: false, @@ -990,6 +995,7 @@ export const data = [ DeviceLocale: 'en-US', EventTypeCode: 'Order Completed', ItemQuantity1: 1, + customImpactProductField1: 'customRSVal', OrderPromoCode: '10OFF-ROCKET', CustomProfileId: '97c46c81-3140-456d-b2a9-690d70aaca35', }, diff --git a/test/integrations/destinations/iterable/processor/aliasTestData.ts b/test/integrations/destinations/iterable/processor/aliasTestData.ts new file mode 100644 index 0000000000..cac43767bb --- /dev/null +++ b/test/integrations/destinations/iterable/processor/aliasTestData.ts @@ -0,0 +1,97 @@ +import { generateMetadata, transformResultBuilder } from './../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const destination: Destination = { + ID: '123', + Name: 'iterable', + DestinationDefinition: { + ID: '123', + Name: 'iterable', + DisplayName: 'Iterable', + Config: {}, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + apiKey: 'testApiKey', + preferUserId: false, + trackAllPages: true, + trackNamedPages: false, + mapToSingleEvent: false, + trackCategorisedPages: false, + }, + Enabled: true, +}; + +const headers = { + api_key: 'testApiKey', + 'Content-Type': 'application/json', +}; + +const properties = { + path: '/abc', + referrer: '', + search: '', + title: '', + url: '', + category: 'test-category', +}; + +const sentAt = '2020-08-28T16:26:16.473Z'; +const originalTimestamp = '2020-08-28T16:26:06.468Z'; + +export const aliasTestData: ProcessorTestData[] = [ + { + id: 'iterable-alias-test-1', + name: 'iterable', + description: 'Alias call with userId and previousId', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update email payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + anonymousId: 'anonId', + userId: 'new@email.com', + previousId: 'old@email.com', + name: 'ApplicationLoaded', + context: {}, + properties, + type: 'alias', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: 'https://api.iterable.com/api/users/updateEmail', + JSON: { + currentEmail: 'old@email.com', + newEmail: 'new@email.com', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/iterable/processor/data.ts b/test/integrations/destinations/iterable/processor/data.ts index 19b370b513..12e5738641 100644 --- a/test/integrations/destinations/iterable/processor/data.ts +++ b/test/integrations/destinations/iterable/processor/data.ts @@ -1,3784 +1,13 @@ +import { identifyTestData } from './identifyTestData'; +import { trackTestData } from './trackTestData'; +import { pageScreenTestData } from './pageScreenTestData'; +import { aliasTestData } from './aliasTestData'; +import { validationTestData } from './validationTestData'; + export const data = [ - { - name: 'iterable', - description: 'Test 0', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'page', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-6f62b91e789a636929ca38aed01c5f6e-103c720d-81bd-4742-98d6-d45a65aed23e', - properties: { - url: 'https://dominos.com', - title: 'Pizza', - referrer: 'https://google.com', - }, - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: 'Invalid page call', - statTags: { - destType: 'ITERABLE', - errorCategory: 'dataValidation', - errorType: 'configuration', - feature: 'processor', - implementation: 'native', - module: 'destination', - }, - statusCode: 400, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 1', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'identify', - sentAt: '2020-08-28T16:26:06.466Z', - traits: { - city: 'Bangalore', - name: 'manashi', - email: 'manashi@website.com', - country: 'India', - }, - context: { - traits: { - city: 'Bangalore', - name: 'manashi', - email: 'manashi@website.com', - country: 'India', - preferUserId: false, - }, - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-cc3ef811f686139ee527b806ee0129ef-163a3a88-266f-447e-8cce-34a8f42f8dcd', - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.462Z', - }, - destination: { - Config: { - preferUserId: false, - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - body: { - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - JSON: { - email: 'manashi@website.com', - userId: 'abcdeeeeeeeexxxx102', - dataFields: { - city: 'Bangalore', - name: 'manashi', - email: 'manashi@website.com', - country: 'India', - }, - preferUserId: false, - mergeNestedObjects: true, - }, - }, - type: 'REST', - files: {}, - method: 'POST', - params: {}, - headers: { - api_key: '62d12498c37c4fd8a1a546c2d35c2f60', - 'Content-Type': 'application/json', - }, - version: '1', - endpoint: 'https://api.iterable.com/api/users/update', - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 2', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'track', - event: 'Email Opened', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-570110489d3e99b234b18af9a9eca9d4-6009779e-82d7-469d-aaeb-5ccf162b0453', - properties: { - subject: 'resume validate', - sendtime: '2020-01-01', - sendlocation: 'akashdeep@gmail.com', - }, - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - body: { - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - JSON: { - userId: 'abcdeeeeeeeexxxx102', - createdAt: 1598631966468, - eventName: 'Email Opened', - dataFields: { - subject: 'resume validate', - sendtime: '2020-01-01', - sendlocation: 'akashdeep@gmail.com', - }, - }, - }, - type: 'REST', - files: {}, - method: 'POST', - params: {}, - headers: { - api_key: '62d12498c37c4fd8a1a546c2d35c2f60', - 'Content-Type': 'application/json', - }, - version: '1', - endpoint: 'https://api.iterable.com/api/events/track', - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 3', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'page', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - }, - userId: '12345', - eventName: 'ApplicationLoaded page', - createdAt: 1571051718299, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 4', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'page', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - campaignId: '123456', - templateId: '1213458', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: true, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - campaignId: '123456', - templateId: '1213458', - }, - userId: '12345', - eventName: 'Loaded a Page', - createdAt: 1571051718299, - campaignId: 123456, - templateId: 1213458, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 5', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'page', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - name: 'test-name', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: false, - trackNamedPages: true, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - name: 'test-name', - }, - userId: '12345', - eventName: 'ApplicationLoaded page', - createdAt: 1571051718299, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 6', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'page', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - }, - userId: '12345', - eventName: 'ApplicationLoaded page', - createdAt: 1571051718299, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 7', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'screen', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - }, - userId: '12345', - eventName: 'ApplicationLoaded screen', - createdAt: 1571051718299, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 8', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'screen', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - campaignId: '123456', - templateId: '1213458', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: true, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - category: 'test-category', - campaignId: '123456', - templateId: '1213458', - }, - userId: '12345', - eventName: 'Loaded a Screen', - createdAt: 1571051718299, - campaignId: 123456, - templateId: 1213458, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 9', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'screen', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - name: 'test-name', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: false, - trackNamedPages: true, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - name: 'test-name', - }, - userId: '12345', - eventName: 'ApplicationLoaded screen', - createdAt: 1571051718299, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 10', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'screen', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'sayan@gmail.com', - dataFields: { - path: '/abc', - referrer: '', - search: '', - title: '', - url: '', - }, - userId: '12345', - eventName: 'ApplicationLoaded screen', - createdAt: 1571051718299, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 11', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'ruchira@rudderlabs.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - id: '72e528f869711c3d', - manufacturer: 'Google', - model: 'sdk_gphone_x86', - name: 'generic_x86_arm', - token: 'some_device_token', - type: 'android', - }, - screen: { - density: 2, - }, - }, - type: 'group', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - anonymousId: '00000000000000000000000000', - userId: '123456', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: 'Message type group not supported', - statTags: { - destType: 'ITERABLE', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - feature: 'processor', - implementation: 'native', - module: 'destination', - }, - statusCode: 400, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 12', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'ruchira@rudderlabs.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - id: '72e528f869711c3d', - manufacturer: 'Google', - model: 'sdk_gphone_x86', - name: 'generic_x86_arm', - token: 'some_device_token', - type: 'android', - }, - screen: { - density: 2, - }, - }, - type: 'identify', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - anonymousId: '00000000000000000000000000', - userId: '123456', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - mergeNestedObjects: false, - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'ruchira@rudderlabs.com', - dataFields: { - email: 'ruchira@rudderlabs.com', - }, - userId: '123456', - preferUserId: true, - mergeNestedObjects: false, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 13', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - device: { - token: 'sample_push_token', - name: 'sample_device_name', - model: 'sample_device_model', - manufacturer: 'sample_device_manufacturer', - type: 'ios', - }, - traits: { - email: 'ruchira@rudderlabs.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - id: '72e528f869711c3d', - manufacturer: 'Google', - model: 'sdk_gphone_x86', - name: 'generic_x86_arm', - token: 'some_device_token', - type: 'android', - }, - screen: { - density: 2, - }, - }, - type: 'identify', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - anonymousId: '00000000000000000000000000', - userId: '123456', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'ruchira@rudderlabs.com', - dataFields: { - email: 'ruchira@rudderlabs.com', - }, - userId: '123456', - preferUserId: true, - mergeNestedObjects: true, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 14', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - device: { - token: 'sample_push_token', - name: 'sample_device_name', - model: 'sample_device_model', - manufacturer: 'sample_device_manufacturer', - type: 'android', - }, - traits: { - email: 'ruchira@rudderlabs.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - id: '72e528f869711c3d', - manufacturer: 'Google', - model: 'sdk_gphone_x86', - name: 'generic_x86_arm', - token: 'some_device_token', - type: 'android', - }, - screen: { - density: 2, - }, - }, - type: 'identify', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - anonymousId: '00000000000000000000000000', - userId: '123456', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'ruchira@rudderlabs.com', - dataFields: { - email: 'ruchira@rudderlabs.com', - }, - userId: '123456', - preferUserId: true, - mergeNestedObjects: true, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 15', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - event: 'product added', - type: 'track', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - campaignId: '1', - templateId: '0', - orderId: 10000, - total: 1000, - products: [ - { - product_id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - price: '19', - position: '1', - category: 'cars', - url: 'https://www.example.com/product/path', - image_url: 'https://www.example.com/product/path.jpg', - quantity: '2', - }, - { - product_id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - price: '192', - quantity: 22, - position: '12', - category: 'Cars2', - url: 'https://www.example.com/product/path2', - image_url: 'https://www.example.com/product/path.jpg2', - }, - ], - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/updateCart', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - user: { - email: 'sayan@gmail.com', - dataFields: { - email: 'sayan@gmail.com', - }, - userId: '12345', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - categories: ['cars'], - price: 19, - quantity: 2, - imageUrl: 'https://www.example.com/product/path.jpg', - url: 'https://www.example.com/product/path', - }, - { - id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - categories: ['Cars2'], - price: 192, - quantity: 22, - imageUrl: 'https://www.example.com/product/path.jpg2', - url: 'https://www.example.com/product/path2', - }, - ], - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 16', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - event: 'order completed', - type: 'track', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - orderId: 10000, - total: '1000', - campaignId: '123456', - templateId: '1213458', - products: [ - { - product_id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - price: '19', - position: '1', - category: 'Cars', - url: 'https://www.example.com/product/path', - image_url: 'https://www.example.com/product/path.jpg', - quantity: 2, - }, - { - product_id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - price: '192', - quantity: '22', - position: '12', - category: 'Cars2', - url: 'https://www.example.com/product/path2', - image_url: 'https://www.example.com/product/path.jpg2', - }, - ], - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/trackPurchase', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - dataFields: { - orderId: 10000, - total: '1000', - campaignId: '123456', - templateId: '1213458', - products: [ - { - product_id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - price: '19', - position: '1', - category: 'Cars', - url: 'https://www.example.com/product/path', - image_url: 'https://www.example.com/product/path.jpg', - quantity: 2, - }, - { - product_id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - price: '192', - quantity: '22', - position: '12', - category: 'Cars2', - url: 'https://www.example.com/product/path2', - image_url: 'https://www.example.com/product/path.jpg2', - }, - ], - }, - id: '10000', - createdAt: 1571051718299, - campaignId: 123456, - templateId: 1213458, - total: 1000, - user: { - email: 'sayan@gmail.com', - dataFields: { - email: 'sayan@gmail.com', - }, - userId: '12345', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - categories: ['Cars'], - price: 19, - quantity: 2, - imageUrl: 'https://www.example.com/product/path.jpg', - url: 'https://www.example.com/product/path', - }, - { - id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - categories: ['Cars2'], - price: 192, - quantity: 22, - imageUrl: 'https://www.example.com/product/path.jpg2', - url: 'https://www.example.com/product/path2', - }, - ], - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 17', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - type: 'track', - messageId: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be', - originalTimestamp: '2019-10-14T11:15:18.300Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - event: 'test track event GA3', - properties: { - email: 'ruchira@rudderlabs.com', - campaignId: '1', - templateId: '0', - category: 'test-category', - user_actual_role: 'system_admin, system_user', - user_actual_id: 12345, - }, - integrations: { - All: true, - }, - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/events/track', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'ruchira@rudderlabs.com', - dataFields: { - campaignId: '1', - templateId: '0', - category: 'test-category', - user_actual_role: 'system_admin, system_user', - user_actual_id: 12345, - email: 'ruchira@rudderlabs.com', - }, - userId: '12345', - eventName: 'test track event GA3', - createdAt: 1571051718300, - campaignId: 1, - templateId: 0, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 18', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - id: '72e528f869711c3d', - manufacturer: 'Google', - model: 'sdk_gphone_x86', - name: 'generic_x86_arm', - token: 'some_device_token', - type: 'android', - }, - screen: { - density: 2, - }, - }, - traits: { - email: 'ruchira@rudderlabs.com', - }, - type: 'identify', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - anonymousId: '00000000000000000000000000', - userId: '123456', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'ruchira@rudderlabs.com', - dataFields: { - email: 'ruchira@rudderlabs.com', - }, - userId: '123456', - preferUserId: true, - mergeNestedObjects: true, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 19', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - id: '72e528f869711c3d', - manufacturer: 'Google', - model: 'sdk_gphone_x86', - name: 'generic_x86_arm', - token: 'some_device_token', - type: 'android', - }, - screen: { - density: 2, - }, - }, - type: 'identify', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: 'userId or email is mandatory for this request', - statTags: { - destType: 'ITERABLE', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - feature: 'processor', - implementation: 'native', - module: 'destination', - }, - statusCode: 400, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 20', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'page', - sentAt: '2020-08-28T16:26:16.473Z', - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-6f62b91e789a636929ca38aed01c5f6e-103c720d-81bd-4742-98d6-d45a65aed23e', - properties: { - url: 'https://dominos.com', - title: 'Pizza', - referrer: 'https://google.com', - }, - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: 'Invalid page call', - statTags: { - destType: 'ITERABLE', - errorCategory: 'dataValidation', - errorType: 'configuration', - feature: 'processor', - implementation: 'native', - module: 'destination', - }, - statusCode: 400, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 21', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'track', - event: 'Product Added', - sentAt: '2021-07-09T05:27:17.908Z', - userId: '8751', - channel: 'web', - context: { - os: { - name: '', - version: '', - }, - app: { - name: 'RudderLabs JavaScript SDK', - build: '1.0.0', - version: '1.0.16', - namespace: 'com.rudderlabs.javascript', - }, - page: { - url: 'https://joybird.com/cabinets/vira-console-cabinet/', - path: '/cabinets/vira-console-cabinet/', - title: 'Vira Console Cabinet | Joybird', - search: '', - referrer: '$direct', - referring_domain: '', - }, - locale: 'en-us', - screen: { - density: 2, - }, - traits: { - email: 'jessica@jlpdesign.net', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.16', - }, - campaign: {}, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15', - }, - rudderId: '1c42e104-97ec-4f54-a328-2379623583fe', - messageId: 'e58f6624-a1c3-48f4-a6af-610389602304', - timestamp: '2021-07-09T05:27:18.131Z', - properties: { - sku: 'JB24691400-W05', - name: 'Vira Console Cabinet', - price: 797, - cart_id: 'bd9b8dbf4ef8ee01d4206b04fe2ee6ae', - variant: 'Oak', - quantity: 1, - quickship: true, - full_price: 1328, - product_id: 10606, - non_interaction: 1, - }, - receivedAt: '2021-07-09T05:27:18.131Z', - request_ip: '162.224.233.114', - anonymousId: '8a7ff986-62d8-45ca-9a16-8895b3f9d341', - integrations: { - All: true, - }, - originalTimestamp: '2021-07-09T05:27:17.908Z', - }, - destination: { - Config: { - credentials: 'abc', - eventToTopicMap: [ - { - from: 'track', - to: 'projects/big-query-integration-poc/topics/test', - }, - ], - }, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/updateCart', - headers: { - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - user: { - email: 'jessica@jlpdesign.net', - dataFields: { - email: 'jessica@jlpdesign.net', - }, - userId: '8751', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: 10606, - sku: 'JB24691400-W05', - name: 'Vira Console Cabinet', - price: 797, - quantity: 1, - }, - ], - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 22', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'sources', - context: { - externalId: [ - { - id: 'lynnanderson@smith.net', - identifierType: 'email', - type: 'ITERABLE-users', - }, - ], - mappedToDestination: 'true', - sources: { - batch_id: 'f5f240d0-0acb-46e0-b043-57fb0aabbadd', - job_id: '1zAj94bEy8komdqnYtSoDp0VmGs/Syncher', - job_run_id: 'c5tar6cqgmgmcjvupdhg', - task_id: 'tt_10_rows_check', - task_run_id: 'c5tar6cqgmgmcjvupdi0', - version: 'release.v1.6.8', - }, - }, - messageId: '2f052f7c-f694-4849-a7ed-a432f7ffa0a4', - originalTimestamp: '2021-10-28T14:03:50.503Z', - receivedAt: '2021-10-28T14:03:46.567Z', - recordId: '8', - request_ip: '10.1.94.92', - rudderId: 'c0f6843e-e3d6-4946-9752-fa339fbadef2', - sentAt: '2021-10-28T14:03:50.503Z', - timestamp: '2021-10-28T14:03:46.566Z', - traits: { - administrative_unit: 'Minnesota', - am_pm: 'AM', - boolean: true, - firstname: 'Jacqueline', - pPower: 'AM', - userId: 'Jacqueline', - }, - type: 'identify', - userId: 'lynnanderson@smith.net', - }, - destination: { - ID: '1zia9wKshXt80YksLmUdJnr7IHI', - Name: 'test_iterable', - DestinationDefinition: { - ID: '1iVQvTRMsPPyJzwol0ifH93QTQ6', - Name: 'ITERABLE', - DisplayName: 'Iterable', - Config: { - destConfig: { - defaultConfig: [ - 'apiKey', - 'mapToSingleEvent', - 'trackAllPages', - 'trackCategorisedPages', - 'trackNamedPages', - ], - }, - excludeKeys: [], - includeKeys: [], - saveDestinationResponse: true, - secretKeys: [], - supportedMessageTypes: ['identify', 'page', 'screen', 'track'], - supportedSourceTypes: [ - 'android', - 'ios', - 'web', - 'unity', - 'amp', - 'cloud', - 'warehouse', - 'reactnative', - 'flutter', - 'cordova', - ], - supportsVisualMapper: true, - transformAt: 'processor', - transformAtV1: 'processor', - }, - ResponseRules: null, - }, - Config: { - apiKey: '12345', - mapToSingleEvent: true, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: true, - }, - Enabled: true, - Transformations: [], - IsProcessorEnabled: true, - }, - libraries: [], - request: { - query: {}, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'lynnanderson@smith.net', - dataFields: { - administrative_unit: 'Minnesota', - am_pm: 'AM', - boolean: true, - firstname: 'Jacqueline', - pPower: 'AM', - userId: 'Jacqueline', - email: 'lynnanderson@smith.net', - }, - userId: 'lynnanderson@smith.net', - preferUserId: true, - mergeNestedObjects: true, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 23', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'sources', - context: { - externalId: [ - { - id: 'Matthew', - identifierType: 'userId', - type: 'ITERABLE-users', - }, - ], - mappedToDestination: 'true', - sources: { - batch_id: '230d7c79-a2c2-4b2a-90bb-06ba988d3bb4', - job_id: '1zjj9aF5UkmavBi4HtM3kWOGvy0/Syncher', - job_run_id: 'c5tb4gsqgmgmcjvuplhg', - task_id: 'tt_10_rows', - task_run_id: 'c5tb4gsqgmgmcjvupli0', - version: 'release.v1.6.8', - }, - }, - messageId: 'c4c97310-463b-4300-9215-5cfddcb2a769', - originalTimestamp: '2021-10-28T14:23:43.254Z', - receivedAt: '2021-10-28T14:23:38.300Z', - recordId: '3', - request_ip: '10.1.94.92', - rudderId: '7300f5e3-bdb5-489e-ac7e-47876e487de9', - sentAt: '2021-10-28T14:23:43.254Z', - timestamp: '2021-10-28T14:23:38.299Z', - traits: { - price: 'GB', - }, - type: 'identify', - userId: 'Matthew', - }, - destination: { - ID: '1zjjHN4RQ6t4DPj3HVpp0b6XW4A', - Name: 'test_userId_uniq', - DestinationDefinition: { - ID: '1iVQvTRMsPPyJzwol0ifH93QTQ6', - Name: 'ITERABLE', - DisplayName: 'Iterable', - Config: { - destConfig: { - defaultConfig: [ - 'apiKey', - 'mapToSingleEvent', - 'trackAllPages', - 'trackCategorisedPages', - 'trackNamedPages', - ], - }, - excludeKeys: [], - includeKeys: [], - saveDestinationResponse: true, - secretKeys: [], - supportedMessageTypes: ['identify', 'page', 'screen', 'track'], - supportedSourceTypes: [ - 'android', - 'ios', - 'web', - 'unity', - 'amp', - 'cloud', - 'warehouse', - 'reactnative', - 'flutter', - 'cordova', - ], - supportsVisualMapper: true, - transformAt: 'processor', - transformAtV1: 'processor', - }, - ResponseRules: null, - }, - Config: { - apiKey: '12345', - mapToSingleEvent: true, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: true, - }, - Enabled: true, - Transformations: [], - IsProcessorEnabled: true, - }, - libraries: [], - request: { - query: {}, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - dataFields: { - price: 'GB', - userId: 'Matthew', - }, - userId: 'Matthew', - preferUserId: true, - mergeNestedObjects: true, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 24', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - device: { - token: 'sample_push_token', - name: 'sample_device_name', - model: 'sample_device_model', - manufacturer: 'sample_device_manufacturer', - type: 'watchos', - }, - traits: { - email: 'ruchira@rudderlabs.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - screen: { - density: 2, - }, - }, - type: 'identify', - messageId: '84e26acc-56a5-4835-8233-591137fca468', - originalTimestamp: '2019-10-14T09:03:17.562Z', - anonymousId: '00000000000000000000000000', - userId: '123456', - integrations: { - All: true, - }, - sentAt: '2019-10-14T09:03:22.563Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/users/update', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - email: 'ruchira@rudderlabs.com', - dataFields: { - email: 'ruchira@rudderlabs.com', - }, - userId: '123456', - preferUserId: true, - mergeNestedObjects: true, - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 25', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'alias', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-6f62b91e789a636929ca38aed01c5f6e-103c720d-81bd-4742-98d6-d45a65aed23e', - properties: { - url: 'https://dominos.com', - title: 'Pizza', - referrer: 'https://google.com', - }, - userId: 'new@email.com', - previousId: 'old@email.com', - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - body: { - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - JSON: { - currentEmail: 'old@email.com', - newEmail: 'new@email.com', - }, - }, - type: 'REST', - files: {}, - method: 'POST', - params: {}, - headers: { - api_key: '62d12498c37c4fd8a1a546c2d35c2f60', - 'Content-Type': 'application/json', - }, - version: '1', - endpoint: 'https://api.iterable.com/api/users/updateEmail', - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 26', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'alias', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-6f62b91e789a636929ca38aed01c5f6e-103c720d-81bd-4742-98d6-d45a65aed23e', - properties: { - url: 'https://dominos.com', - title: 'Pizza', - referrer: 'https://google.com', - }, - userId: 'new@email.com', - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: 'Missing required value from "previousId"', - statTags: { - destType: 'ITERABLE', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - feature: 'processor', - implementation: 'native', - module: 'destination', - }, - statusCode: 400, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 27', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'alias', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-6f62b91e789a636929ca38aed01c5f6e-103c720d-81bd-4742-98d6-d45a65aed23e', - properties: { - url: 'https://dominos.com', - title: 'Pizza', - referrer: 'https://google.com', - }, - previousId: 'old@email.com', - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: false, - trackCategorisedPages: true, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - error: 'Missing required value from "userId"', - statTags: { - destType: 'ITERABLE', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - feature: 'processor', - implementation: 'native', - module: 'destination', - }, - statusCode: 400, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 28', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'john@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - event: 'product added', - type: 'track', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - campaignId: '1', - templateId: '0', - orderId: 10000, - total: 1000, - products: [ - { - product_id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - price: '19', - position: '1', - category: ['bikes', 'cars', 'motors'], - url: 'https://www.example.com/product/path', - image_url: 'https://www.example.com/product/path.jpg', - quantity: '2', - }, - { - product_id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - price: '192', - quantity: 22, - position: '12', - category: ['Bikes2', 'cars2', 'motors2'], - url: 'https://www.example.com/product/path2', - image_url: 'https://www.example.com/product/path.jpg2', - }, - ], - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/updateCart', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - user: { - email: 'john@gmail.com', - dataFields: { - email: 'john@gmail.com', - }, - userId: '12345', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - categories: ['bikes', 'cars', 'motors'], - price: 19, - quantity: 2, - imageUrl: 'https://www.example.com/product/path.jpg', - url: 'https://www.example.com/product/path', - }, - { - id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - categories: ['Bikes2', 'cars2', 'motors2'], - price: 192, - quantity: 22, - imageUrl: 'https://www.example.com/product/path.jpg2', - url: 'https://www.example.com/product/path2', - }, - ], - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 29', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - event: 'product added', - type: 'track', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - campaignId: '1', - templateId: '0', - orderId: 10000, - total: 1000, - products: [ - { - product_id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - price: '19', - position: '1', - category: 'shirts,pants,trousers', - url: 'https://www.example.com/product/path', - image_url: 'https://www.example.com/product/path.jpg', - quantity: '2', - }, - { - product_id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - price: '192', - quantity: 22, - position: '12', - category: 'Cars2', - url: 'https://www.example.com/product/path2', - image_url: 'https://www.example.com/product/path.jpg2', - }, - ], - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/updateCart', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - user: { - email: 'sayan@gmail.com', - dataFields: { - email: 'sayan@gmail.com', - }, - userId: '12345', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: '507f1f77bcf86cd799439011', - sku: '45790-32', - name: 'Monopoly: 3rd Edition', - categories: ['shirts', 'pants', 'trousers'], - price: 19, - quantity: 2, - imageUrl: 'https://www.example.com/product/path.jpg', - url: 'https://www.example.com/product/path', - }, - { - id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - categories: ['Cars2'], - price: 192, - quantity: 22, - imageUrl: 'https://www.example.com/product/path.jpg2', - url: 'https://www.example.com/product/path2', - }, - ], - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 30', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - event: 'product added', - type: 'track', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - campaignId: '1', - templateId: '0', - orderId: 10000, - total: 1000, - product_id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - price: '192', - quantity: 22, - position: '12', - category: 'Cars2', - url: 'https://www.example.com/product/path2', - image_url: 'https://www.example.com/product/path.jpg2', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - destination: { - Config: { - apiKey: '12345', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/updateCart', - headers: { - 'Content-Type': 'application/json', - api_key: '12345', - }, - params: {}, - body: { - JSON: { - user: { - email: 'sayan@gmail.com', - dataFields: { - email: 'sayan@gmail.com', - }, - userId: '12345', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: '507f1f77bcf86cd7994390112', - sku: '45790-322', - name: 'Monopoly: 3rd Edition2', - categories: ['Cars2'], - price: 192, - quantity: 22, - imageUrl: 'https://www.example.com/product/path.jpg2', - url: 'https://www.example.com/product/path2', - }, - ], - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 31', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - destination: { - Config: { - passcode: - 'fbee74a147828e2932c701d19dc1f2dcfa4ac0048be3aa3a88d427090a59dc1c0fa002f1', - accountId: '476550467', - trackAnonymous: true, - enableObjectIdMapping: false, - }, - }, - message: { - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.0.0', - }, - traits: { - email: 'sayan@gmail.com', - }, - library: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - locale: 'en-US', - ip: '0.0.0.0', - os: { - name: '', - version: '', - }, - screen: { - density: 2, - }, - }, - event: 'order completed', - type: 'track', - messageId: '5e10d13a-bf9a-44bf-b884-43a9e591ea71', - originalTimestamp: '2019-10-14T11:15:18.299Z', - anonymousId: '00000000000000000000000000', - userId: '12345', - properties: { - product_id: 1234, - name: 'Shoes', - price: 45, - quantity: 1, - orderId: 10000, - total: '1000', - campaignId: '123456', - templateId: '1213458', - }, - integrations: { - All: true, - }, - name: 'ApplicationLoaded', - sentAt: '2019-10-14T11:15:53.296Z', - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.iterable.com/api/commerce/trackPurchase', - headers: { - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - dataFields: { - product_id: 1234, - name: 'Shoes', - price: 45, - quantity: 1, - orderId: 10000, - total: '1000', - campaignId: '123456', - templateId: '1213458', - }, - id: '10000', - createdAt: 1571051718299, - campaignId: 123456, - templateId: 1213458, - total: 1000, - user: { - email: 'sayan@gmail.com', - dataFields: { - email: 'sayan@gmail.com', - }, - userId: '12345', - preferUserId: true, - mergeNestedObjects: true, - }, - items: [ - { - id: 1234, - name: 'Shoes', - price: 45, - quantity: 1, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 32', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'track', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-570110489d3e99b234b18af9a9eca9d4-6009779e-82d7-469d-aaeb-5ccf162b0453', - properties: { - subject: 'resume validate', - sendtime: '2020-01-01', - sendlocation: 'akashdeep@gmail.com', - }, - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - body: { - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - JSON: { - userId: 'abcdeeeeeeeexxxx102', - createdAt: 1598631966468, - dataFields: { - subject: 'resume validate', - sendtime: '2020-01-01', - sendlocation: 'akashdeep@gmail.com', - }, - }, - }, - type: 'REST', - files: {}, - method: 'POST', - params: {}, - headers: { - api_key: '62d12498c37c4fd8a1a546c2d35c2f60', - 'Content-Type': 'application/json', - }, - version: '1', - endpoint: 'https://api.iterable.com/api/events/track', - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, - { - name: 'iterable', - description: 'Test 33', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - message: { - type: 'track', - event: '', - sentAt: '2020-08-28T16:26:16.473Z', - context: { - library: { - name: 'analytics-node', - version: '0.0.3', - }, - }, - _metadata: { - nodeVersion: '10.22.0', - }, - messageId: - 'node-570110489d3e99b234b18af9a9eca9d4-6009779e-82d7-469d-aaeb-5ccf162b0453', - properties: { - subject: 'resume validate', - sendtime: '2020-01-01', - sendlocation: 'akashdeep@gmail.com', - }, - anonymousId: 'abcdeeeeeeeexxxx102', - originalTimestamp: '2020-08-28T16:26:06.468Z', - }, - destination: { - Config: { - apiKey: '62d12498c37c4fd8a1a546c2d35c2f60', - mapToSingleEvent: false, - trackAllPages: true, - trackCategorisedPages: false, - trackNamedPages: false, - }, - Enabled: true, - }, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - output: { - body: { - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - JSON: { - userId: 'abcdeeeeeeeexxxx102', - createdAt: 1598631966468, - dataFields: { - subject: 'resume validate', - sendtime: '2020-01-01', - sendlocation: 'akashdeep@gmail.com', - }, - }, - }, - type: 'REST', - files: {}, - method: 'POST', - params: {}, - headers: { - api_key: '62d12498c37c4fd8a1a546c2d35c2f60', - 'Content-Type': 'application/json', - }, - version: '1', - endpoint: 'https://api.iterable.com/api/events/track', - userId: '', - }, - statusCode: 200, - }, - ], - }, - }, - }, + ...identifyTestData, + ...trackTestData, + ...pageScreenTestData, + ...aliasTestData, + ...validationTestData, ]; diff --git a/test/integrations/destinations/iterable/processor/identifyTestData.ts b/test/integrations/destinations/iterable/processor/identifyTestData.ts new file mode 100644 index 0000000000..d05f87a11f --- /dev/null +++ b/test/integrations/destinations/iterable/processor/identifyTestData.ts @@ -0,0 +1,407 @@ +import { + generateMetadata, + transformResultBuilder, + generateIndentifyPayload, +} from './../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const destination: Destination = { + ID: '123', + Name: 'iterable', + DestinationDefinition: { + ID: '123', + Name: 'iterable', + DisplayName: 'Iterable', + Config: {}, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + apiKey: 'testApiKey', + preferUserId: false, + trackAllPages: true, + trackNamedPages: false, + mapToSingleEvent: false, + trackCategorisedPages: false, + }, + Enabled: true, +}; + +const headers = { + api_key: 'testApiKey', + 'Content-Type': 'application/json', +}; + +const user1Traits = { + name: 'manashi', + country: 'India', + city: 'Bangalore', + email: 'manashi@website.com', +}; + +const user2Traits = { + am_pm: 'AM', + pPower: 'AM', + boolean: true, + userId: 'Jacqueline', + firstname: 'Jacqueline', + administrative_unit: 'Minnesota', +}; + +const userId = 'userId'; +const anonymousId = 'anonId'; +const sentAt = '2020-08-28T16:26:16.473Z'; +const originalTimestamp = '2020-08-28T16:26:06.468Z'; + +const updateUserEndpoint = 'https://api.iterable.com/api/users/update'; + +export const identifyTestData: ProcessorTestData[] = [ + { + id: 'iterable-identify-test-1', + name: 'iterable', + description: 'Indentify call to update user in iterable with user traits', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update user payload with all user traits', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + anonymousId, + context: { + traits: user1Traits, + }, + traits: user1Traits, + type: 'identify', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateUserEndpoint, + JSON: { + email: user1Traits.email, + userId: anonymousId, + dataFields: user1Traits, + preferUserId: false, + mergeNestedObjects: true, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-identify-test-2', + name: 'iterable', + description: 'Indentify call to update user email', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update user payload with new email sent in payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateIndentifyPayload({ + userId, + anonymousId, + context: { + traits: { email: 'ruchira@rudderlabs.com' }, + }, + type: 'identify', + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateUserEndpoint, + JSON: { + email: 'ruchira@rudderlabs.com', + userId, + dataFields: { + email: 'ruchira@rudderlabs.com', + }, + preferUserId: false, + mergeNestedObjects: true, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-identify-test-3', + name: 'iterable', + description: 'Indentify call to update user email with preferUserId config set to true', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update user payload with new email sent in payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { ...destination, Config: { ...destination.Config, preferUserId: true } }, + message: generateIndentifyPayload({ + userId, + anonymousId, + context: { + traits: { email: 'ruchira@rudderlabs.com' }, + }, + type: 'identify', + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateUserEndpoint, + JSON: { + email: 'ruchira@rudderlabs.com', + userId, + dataFields: { + email: 'ruchira@rudderlabs.com', + }, + preferUserId: true, + mergeNestedObjects: true, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-identify-test-4', + name: 'iterable', + description: + 'Indentify call to update user email with traits present at root instead of context.traits', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update user payload with new email sent in payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { ...destination, Config: { ...destination.Config, preferUserId: true } }, + message: generateIndentifyPayload({ + userId, + anonymousId, + context: { + traits: {}, + }, + traits: { email: 'ruchira@rudderlabs.com' }, + type: 'identify', + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateUserEndpoint, + JSON: { + email: 'ruchira@rudderlabs.com', + userId, + dataFields: { + email: 'ruchira@rudderlabs.com', + }, + preferUserId: true, + mergeNestedObjects: true, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-identify-test-5', + name: 'iterable', + description: 'Iterable rEtl test to update user', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update user payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { ...destination, Config: { ...destination.Config, preferUserId: true } }, + message: { + userId, + anonymousId, + context: { + externalId: [ + { + id: 'lynnanderson@smith.net', + identifierType: 'email', + type: 'ITERABLE-users', + }, + ], + mappedToDestination: 'true', + }, + traits: user2Traits, + type: 'identify', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateUserEndpoint, + JSON: { + email: 'lynnanderson@smith.net', + userId, + dataFields: { ...user2Traits, email: 'lynnanderson@smith.net' }, + preferUserId: true, + mergeNestedObjects: true, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-identify-test-6', + name: 'iterable', + description: 'Iterable rEtl test to update user traits', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain update user payload', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + userId: 'Matthew', + anonymousId, + context: { + externalId: [ + { + id: 'Matthew', + identifierType: 'userId', + type: 'ITERABLE-users', + }, + ], + mappedToDestination: 'true', + }, + traits: user2Traits, + type: 'identify', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateUserEndpoint, + JSON: { + userId: 'Matthew', + dataFields: { ...user2Traits, userId: 'Matthew' }, + preferUserId: false, + mergeNestedObjects: true, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/iterable/processor/pageScreenTestData.ts b/test/integrations/destinations/iterable/processor/pageScreenTestData.ts new file mode 100644 index 0000000000..074d6b56df --- /dev/null +++ b/test/integrations/destinations/iterable/processor/pageScreenTestData.ts @@ -0,0 +1,409 @@ +import { generateMetadata, transformResultBuilder } from './../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const destination: Destination = { + ID: '123', + Name: 'iterable', + DestinationDefinition: { + ID: '123', + Name: 'iterable', + DisplayName: 'Iterable', + Config: {}, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + apiKey: 'testApiKey', + preferUserId: false, + trackAllPages: true, + trackNamedPages: false, + mapToSingleEvent: false, + trackCategorisedPages: false, + }, + Enabled: true, +}; + +const headers = { + api_key: 'testApiKey', + 'Content-Type': 'application/json', +}; + +const properties = { + path: '/abc', + referrer: '', + search: '', + title: '', + url: '', + category: 'test-category', +}; + +const anonymousId = 'anonId'; +const sentAt = '2020-08-28T16:26:16.473Z'; +const originalTimestamp = '2020-08-28T16:26:06.468Z'; + +const pageEndpoint = 'https://api.iterable.com/api/events/track'; + +export const pageScreenTestData: ProcessorTestData[] = [ + { + id: 'iterable-page-test-1', + name: 'iterable', + description: 'Page call with name and properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain page name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + anonymousId, + name: 'ApplicationLoaded', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties, + type: 'page', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: pageEndpoint, + JSON: { + userId: anonymousId, + dataFields: properties, + email: 'sayan@gmail.com', + createdAt: 1598631966468, + eventName: 'ApplicationLoaded page', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-page-test-2', + name: 'iterable', + description: 'Page call with name and properties and mapToSingleEvent config set to true', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain page name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + ...destination, + Config: { ...destination.Config, mapToSingleEvent: true }, + }, + message: { + anonymousId, + name: 'ApplicationLoaded', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties: { ...properties, campaignId: '123456', templateId: '1213458' }, + type: 'page', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: pageEndpoint, + JSON: { + campaignId: 123456, + templateId: 1213458, + userId: anonymousId, + email: 'sayan@gmail.com', + createdAt: 1598631966468, + eventName: 'Loaded a Page', + dataFields: { ...properties, campaignId: '123456', templateId: '1213458' }, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-page-test-3', + name: 'iterable', + description: 'Page call with name and properties and trackNamedPages config set to true', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain page name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + ...destination, + Config: { ...destination.Config, trackNamedPages: true, trackAllPages: false }, + }, + message: { + anonymousId, + name: 'ApplicationLoaded', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties, + type: 'page', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: pageEndpoint, + JSON: { + userId: anonymousId, + email: 'sayan@gmail.com', + createdAt: 1598631966468, + eventName: 'ApplicationLoaded page', + dataFields: properties, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-screen-test-1', + name: 'iterable', + description: 'Screen call with name and properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain screen name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + ...destination, + Config: { ...destination.Config, trackCategorisedPages: true, trackAllPages: false }, + }, + message: { + anonymousId, + name: 'ApplicationLoaded', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties, + type: 'screen', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: pageEndpoint, + JSON: { + userId: anonymousId, + dataFields: properties, + email: 'sayan@gmail.com', + createdAt: 1598631966468, + eventName: 'ApplicationLoaded screen', + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-screen-test-2', + name: 'iterable', + description: 'Screen call with name and properties and mapToSingleEvent config set to true', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain screen name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + ...destination, + Config: { ...destination.Config, mapToSingleEvent: true }, + }, + message: { + anonymousId, + name: 'ApplicationLoaded', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties: { ...properties, campaignId: '123456', templateId: '1213458' }, + type: 'screen', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: pageEndpoint, + JSON: { + campaignId: 123456, + templateId: 1213458, + userId: anonymousId, + email: 'sayan@gmail.com', + createdAt: 1598631966468, + eventName: 'Loaded a Screen', + dataFields: { ...properties, campaignId: '123456', templateId: '1213458' }, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-screen-test-3', + name: 'iterable', + description: 'Page call with name and properties and trackNamedPages config set to true', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain page name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + ...destination, + Config: { ...destination.Config, trackNamedPages: true, trackAllPages: false }, + }, + message: { + anonymousId, + name: 'ApplicationLoaded', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties, + type: 'screen', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: pageEndpoint, + JSON: { + userId: anonymousId, + email: 'sayan@gmail.com', + createdAt: 1598631966468, + eventName: 'ApplicationLoaded screen', + dataFields: properties, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/iterable/processor/trackTestData.ts b/test/integrations/destinations/iterable/processor/trackTestData.ts new file mode 100644 index 0000000000..296275ad77 --- /dev/null +++ b/test/integrations/destinations/iterable/processor/trackTestData.ts @@ -0,0 +1,717 @@ +import { + generateMetadata, + generateTrackPayload, + transformResultBuilder, +} from './../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const destination: Destination = { + ID: '123', + Name: 'iterable', + DestinationDefinition: { + ID: '123', + Name: 'iterable', + DisplayName: 'Iterable', + Config: {}, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + apiKey: 'testApiKey', + preferUserId: false, + trackAllPages: true, + trackNamedPages: false, + mapToSingleEvent: false, + trackCategorisedPages: false, + }, + Enabled: true, +}; + +const headers = { + api_key: 'testApiKey', + 'Content-Type': 'application/json', +}; + +const properties = { + subject: 'resume validate', + sendtime: '2020-01-01', + sendlocation: 'akashdeep@gmail.com', +}; + +const customEventProperties = { + campaignId: '1', + templateId: '0', + user_actual_id: 12345, + category: 'test-category', + email: 'ruchira@rudderlabs.com', + user_actual_role: 'system_admin, system_user', +}; + +const productInfo = { + price: 797, + variant: 'Oak', + quantity: 1, + quickship: true, + full_price: 1328, + product_id: 10606, + non_interaction: 1, + sku: 'JB24691400-W05', + name: 'Vira Console Cabinet', + cart_id: 'bd9b8dbf4ef8ee01d4206b04fe2ee6ae', +}; + +const orderCompletedProductInfo = { + price: 45, + quantity: 1, + total: '1000', + name: 'Shoes', + orderId: 10000, + product_id: 1234, + campaignId: '123456', + templateId: '1213458', +}; + +const products = [ + { + product_id: '507f1f77bcf86cd799439011', + sku: '45790-32', + name: 'Monopoly: 3rd Edition', + price: '19', + position: '1', + category: 'cars', + url: 'https://www.example.com/product/path', + image_url: 'https://www.example.com/product/path.jpg', + quantity: '2', + }, + { + product_id: '507f1f77bcf86cd7994390112', + sku: '45790-322', + name: 'Monopoly: 3rd Edition2', + price: '192', + quantity: 22, + position: '12', + category: 'Cars2', + url: 'https://www.example.com/product/path2', + image_url: 'https://www.example.com/product/path.jpg2', + }, +]; + +const items = [ + { + id: '507f1f77bcf86cd799439011', + sku: '45790-32', + name: 'Monopoly: 3rd Edition', + categories: ['cars'], + price: 19, + quantity: 2, + imageUrl: 'https://www.example.com/product/path.jpg', + url: 'https://www.example.com/product/path', + }, + { + id: '507f1f77bcf86cd7994390112', + sku: '45790-322', + name: 'Monopoly: 3rd Edition2', + categories: ['Cars2'], + price: 192, + quantity: 22, + imageUrl: 'https://www.example.com/product/path.jpg2', + url: 'https://www.example.com/product/path2', + }, +]; + +const userId = 'userId'; +const anonymousId = 'anonId'; +const sentAt = '2020-08-28T16:26:16.473Z'; +const originalTimestamp = '2020-08-28T16:26:06.468Z'; + +const endpoint = 'https://api.iterable.com/api/events/track'; +const updateCartEndpoint = 'https://api.iterable.com/api/commerce/updateCart'; +const trackPurchaseEndpoint = 'https://api.iterable.com/api/commerce/trackPurchase'; + +export const trackTestData: ProcessorTestData[] = [ + { + id: 'iterable-track-test-1', + name: 'iterable', + description: 'Track call to add event with user', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event properties and event name', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + anonymousId, + event: 'Email Opened', + type: 'track', + context: {}, + properties, + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint, + JSON: { + userId: 'anonId', + createdAt: 1598631966468, + eventName: 'Email Opened', + dataFields: properties, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-2', + name: 'iterable', + description: 'Track call for product added event with all properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateTrackPayload({ + userId, + anonymousId, + event: 'product added', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties: { + campaignId: '1', + templateId: '0', + orderId: 10000, + total: 1000, + products, + }, + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateCartEndpoint, + JSON: { + user: { + email: 'sayan@gmail.com', + dataFields: { + email: 'sayan@gmail.com', + }, + userId, + preferUserId: false, + mergeNestedObjects: true, + }, + items, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-3', + name: 'iterable', + description: 'Track call for order completed event with all properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateTrackPayload({ + userId, + anonymousId, + event: 'order completed', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties: { + orderId: 10000, + total: '1000', + campaignId: '123456', + templateId: '1213458', + products, + }, + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: trackPurchaseEndpoint, + JSON: { + dataFields: { + orderId: 10000, + total: '1000', + campaignId: '123456', + templateId: '1213458', + products, + }, + id: '10000', + createdAt: 1598631966468, + campaignId: 123456, + templateId: 1213458, + total: 1000, + user: { + email: 'sayan@gmail.com', + dataFields: { + email: 'sayan@gmail.com', + }, + userId, + preferUserId: false, + mergeNestedObjects: true, + }, + items, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-4', + name: 'iterable', + description: 'Track call for custom event with all properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain custom event name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateTrackPayload({ + userId, + anonymousId, + event: 'test track event GA3', + context: { + traits: { + email: 'sayan@gmail.com', + }, + }, + properties: customEventProperties, + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint, + JSON: { + email: 'ruchira@rudderlabs.com', + dataFields: customEventProperties, + userId, + eventName: 'test track event GA3', + createdAt: 1598631966468, + campaignId: 1, + templateId: 0, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-5', + name: 'iterable', + description: 'Track call for product added event with product info as properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateTrackPayload({ + userId, + anonymousId, + event: 'product added', + context: { + traits: { + email: 'jessica@jlpdesign.net', + }, + }, + properties: productInfo, + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateCartEndpoint, + JSON: { + user: { + email: 'jessica@jlpdesign.net', + dataFields: { + email: 'jessica@jlpdesign.net', + }, + userId, + preferUserId: false, + mergeNestedObjects: true, + }, + items: [ + { + id: productInfo.product_id, + sku: productInfo.sku, + name: productInfo.name, + price: productInfo.price, + quantity: productInfo.quantity, + }, + ], + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-6', + name: 'iterable', + description: 'Track call for product added event with product info as properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateTrackPayload({ + userId, + anonymousId, + event: 'product added', + context: { + traits: { + email: 'jessica@jlpdesign.net', + }, + }, + properties: { + campaignId: '1', + templateId: '0', + orderId: 10000, + total: 1000, + ...products[1], + }, + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: updateCartEndpoint, + JSON: { + user: { + email: 'jessica@jlpdesign.net', + dataFields: { + email: 'jessica@jlpdesign.net', + }, + userId, + preferUserId: false, + mergeNestedObjects: true, + }, + items: [ + { + price: 192, + url: products[1].url, + sku: products[1].sku, + name: products[1].name, + id: products[1].product_id, + quantity: products[1].quantity, + imageUrl: products[1].image_url, + categories: [products[1].category], + }, + ], + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-7', + name: 'iterable', + description: 'Track call for order completed event with product info as properties', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event name and all properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: generateTrackPayload({ + userId, + anonymousId, + event: 'order completed', + context: { + traits: { + email: 'jessica@jlpdesign.net', + }, + }, + properties: orderCompletedProductInfo, + sentAt, + originalTimestamp, + }), + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint: trackPurchaseEndpoint, + JSON: { + dataFields: orderCompletedProductInfo, + user: { + email: 'jessica@jlpdesign.net', + dataFields: { + email: 'jessica@jlpdesign.net', + }, + userId, + preferUserId: false, + mergeNestedObjects: true, + }, + id: '10000', + total: 1000, + campaignId: 123456, + templateId: 1213458, + createdAt: 1598631966468, + items: [ + { + id: orderCompletedProductInfo.product_id, + name: orderCompletedProductInfo.name, + price: orderCompletedProductInfo.price, + quantity: orderCompletedProductInfo.quantity, + }, + ], + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-8', + name: 'iterable', + description: 'Track call without event name and userId', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + anonymousId, + type: 'track', + context: {}, + properties, + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint, + JSON: { + userId: anonymousId, + createdAt: 1598631966468, + dataFields: properties, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-track-test-8', + name: 'iterable', + description: 'Track call without event name', + scenario: 'Business', + successCriteria: + 'Response should contain status code 200 and it should contain event properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + userId, + anonymousId, + type: 'track', + context: {}, + properties, + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + headers, + endpoint, + JSON: { + userId, + createdAt: 1598631966468, + dataFields: properties, + }, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/iterable/processor/validationTestData.ts b/test/integrations/destinations/iterable/processor/validationTestData.ts new file mode 100644 index 0000000000..86728a868b --- /dev/null +++ b/test/integrations/destinations/iterable/processor/validationTestData.ts @@ -0,0 +1,258 @@ +import { generateMetadata } from './../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const destination: Destination = { + ID: '123', + Name: 'iterable', + DestinationDefinition: { + ID: '123', + Name: 'iterable', + DisplayName: 'Iterable', + Config: {}, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + apiKey: 'testApiKey', + mapToSingleEvent: false, + trackAllPages: false, + trackCategorisedPages: true, + trackNamedPages: false, + }, + Enabled: true, +}; + +const properties = { + url: 'https://dominos.com', + title: 'Pizza', + referrer: 'https://google.com', +}; + +const sentAt = '2020-08-28T16:26:16.473Z'; +const originalTimestamp = '2020-08-28T16:26:06.468Z'; + +const expectedStatTags = { + destType: 'ITERABLE', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'native', + module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', +}; + +export const validationTestData: ProcessorTestData[] = [ + { + id: 'iterable-validation-test-1', + name: 'iterable', + description: "[Error]: Page call without it's required configuration", + scenario: 'Framework', + successCriteria: + 'Response should contain status code 400 and it should throw configuration error with respective message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + userId: 'sajal12', + anonymousId: 'abcdeeeeeeeexxxx102', + context: { + traits: { + email: 'abc@example.com', + }, + }, + properties, + type: 'page', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 400, + error: 'Invalid page call', + statTags: { ...expectedStatTags, errorType: 'configuration' }, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-validation-test-2', + name: 'iterable', + description: '[Error]: Identify call without userId and email', + scenario: 'Framework', + successCriteria: + 'Response should contain status code 400 and it should throw instrumentation error with respective message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: {}, + type: 'identify', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 400, + error: 'userId or email is mandatory for this request', + statTags: expectedStatTags, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-validation-test-3', + name: 'iterable', + description: '[Error]: Message type is not supported', + scenario: 'Framework', + successCriteria: + 'Response should contain status code 400 and it should throw instrumentation error with respective message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: {}, + type: 'group', + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 400, + error: 'Message type group not supported', + statTags: expectedStatTags, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-validation-test-4', + name: 'iterable', + description: '[Error]: Missing required value for alias call', + scenario: 'Framework', + successCriteria: + 'Response should contain status code 400 and it should throw instrumentation error with respective message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: {}, + type: 'alias', + properties, + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 400, + error: 'Missing required value from "previousId"', + statTags: expectedStatTags, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'iterable-validation-test-5', + name: 'iterable', + description: '[Error]: Missing userId value for alias call', + scenario: 'Framework', + successCriteria: + 'Response should contain status code 400 and it should throw instrumentation error with respective message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: {}, + type: 'alias', + previousId: 'old@email.com', + anonymousId: 'anonId', + properties, + sentAt, + originalTimestamp, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 400, + error: 'Missing required value from "userId"', + statTags: expectedStatTags, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_ads/dataDelivery/business.ts b/test/integrations/destinations/linkedin_ads/dataDelivery/business.ts new file mode 100644 index 0000000000..ff4fa4455f --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/dataDelivery/business.ts @@ -0,0 +1,188 @@ +import { generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; + +export const element = { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '0', + currencyCode: 'USD', + }, + eventId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'abc@gmail.com', + }, + ], + }, +}; + +export const wrongFormatElement = { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + currencyCode: 'USD', + }, + eventId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'abc@gmail.com', + }, + ], + userInfo: { + city: 'San Francisco', + }, + }, +}; + +export const testJSONData = { + elements: [{ ...element }], +}; + +export const wrongFormattedTestJSONData = { + elements: [{ ...wrongFormatElement }], +}; + +export const testJSONDataWithDifferentTypeConversion = { + elements: [ + { + ...element, + conversion: 'urn:li:partner:differentConversion', + }, + ], +}; + +export const statTags = { + destType: 'LINKEDIN_ADS', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const metadata = { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, +}; +export const headerBlockWithCorrectAccessToken = { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202402', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', +}; + +const commonRequestParametersWithWrongElemet = { + headers: headerBlockWithCorrectAccessToken, + JSON: wrongFormattedTestJSONData, +}; + +const commonRequestParametersWithDifferentConversion = { + headers: headerBlockWithCorrectAccessToken, + JSON: testJSONDataWithDifferentTypeConversion, +}; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'linkedin_ads_v1_scenario_1', + name: 'linkedin_ads', + description: 'Event fails due to wrong process followed while creating a conversion', + successCriteria: 'Should return 400 and aborted', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + ...commonRequestParametersWithDifferentConversion, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: + "LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. Incorrect conversions information provided. Conversion's method should be CONVERSIONS_API, indices [0] (0-indexed)", + response: [ + { + error: + '{"message":"Incorrect conversions information provided. Conversion\'s method should be CONVERSIONS_API, indices [0] (0-indexed)","status":400}', + statusCode: 400, + metadata, + }, + ], + statTags, + status: 400, + }, + }, + }, + }, + }, + { + id: 'linkedin_ads_v1_scenario_2', + name: 'linkedin_ads', + description: 'Event fails due to wrong format payload sent to linkedin', + successCriteria: 'Should return 400 with appropriate reason of failure', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + ...commonRequestParametersWithWrongElemet, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + destinationResponse: { + response: { + message: + 'Index: 0, ERROR :: /conversionValue/amount :: field is required but not found and has no default value\nERROR :: /user/userInfo/firstName :: field is required but not found and has no default value\nERROR :: /user/userInfo/lastName :: field is required but not found and has no default value\n', + status: 422, + }, + status: 422, + }, + message: + '[LINKEDIN_CONVERSION_API Response V1 Handler] - Request Processed Successfully', + response: [ + { + error: + '/conversionValue/amount :: field is required but not found and has no default value', + statusCode: 400, + metadata, + }, + ], + status: 422, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_ads/dataDelivery/data.ts b/test/integrations/destinations/linkedin_ads/dataDelivery/data.ts new file mode 100644 index 0000000000..5bb0a7ef6e --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/dataDelivery/data.ts @@ -0,0 +1,4 @@ +import { testScenariosForV1API, statTags as baseStatTags } from './business'; +import { oauthScenariosV1 } from './oauth'; + +export const data = [...testScenariosForV1API, ...oauthScenariosV1]; diff --git a/test/integrations/destinations/linkedin_ads/dataDelivery/oauth.ts b/test/integrations/destinations/linkedin_ads/dataDelivery/oauth.ts new file mode 100644 index 0000000000..5cc643d972 --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/dataDelivery/oauth.ts @@ -0,0 +1,207 @@ +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { ProxyV1TestData } from '../../../testTypes'; + +export const testJSONData = { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '0', + currencyCode: 'USD', + }, + eventId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'abc@gmail.com', + }, + ], + }, + }, + ], +}; +export const statTags = { + destType: 'LINKEDIN_ADS', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; + +export const metadata = { + jobId: 1, + attemptNum: 1, + userId: 'default-userId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + sourceId: 'default-sourceId', + secret: { + accessToken: 'default-accessToken', + }, + dontBatch: false, +}; + +export const headerBlockWithCorrectAccessToken = { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202402', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', +}; + +const commonRequestParameters = { + headers: headerBlockWithCorrectAccessToken, + JSON: testJSONData, +}; +const commonRequestParametersWithInvalidAccess = { + headers: { ...headerBlockWithCorrectAccessToken, Authorization: 'Bearer invalidToken' }, + JSON: testJSONData, + accessToken: 'invalidToken', +}; + +const commonRequestParametersWithRevokedAccess = { + headers: { ...headerBlockWithCorrectAccessToken, Authorization: 'Bearer revokedToken' }, + JSON: testJSONData, + accessToken: 'revokedToken', +}; + +export const oauthScenariosV1: ProxyV1TestData[] = [ + { + id: 'linkedin_ads_v1_oauth_scenario_1', + name: 'linkedin_ads', + description: 'app event fails due to revoked access token error', + successCriteria: 'Should return 400 with revoked access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + ...commonRequestParametersWithRevokedAccess, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + response: [ + { + error: + '{"status":401,"serviceErrorCode":65601,"code":"REVOKED_ACCESS_TOKEN","message":"The token used in the request has been revoked by the user"}', + statusCode: 400, + metadata: { ...metadata, secret: { accessToken: 'revokedToken' } }, + }, + ], + statTags, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + 'LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. Invalid or expired access token. Retrying', + status: 400, + }, + }, + }, + }, + }, + { + id: 'linkedin_ads_v1_oauth_scenario_2', + name: 'linkedin_ads', + description: 'app event fails due to invalid access token error', + successCriteria: 'Should return 500 with invalid access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + ...commonRequestParametersWithInvalidAccess, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + response: [ + { + error: + '{"status":401,"serviceErrorCode":65600,"code":"INVALID_ACCESS_TOKEN","message":"Invalid access token"}', + statusCode: 500, + metadata: { ...metadata, secret: { accessToken: 'invalidToken' } }, + }, + ], + statTags: { ...statTags, errorType: 'retryable' }, + authErrorCategory: 'REFRESH_TOKEN', + message: + 'LinkedIn Conversion API: Error transformer proxy v1 during LinkedIn Conversion API response transformation. Invalid or expired access token. Retrying', + status: 500, + }, + }, + }, + }, + }, + { + id: 'linkedin_ads_v1_oauth_scenario_3', + name: 'linkedin_ads', + description: 'success case', + successCriteria: 'Should return 200 response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + ...commonRequestParameters, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: + '[LINKEDIN_CONVERSION_API Response V1 Handler] - Request Processed Successfully', + destinationResponse: { + response: { + elements: [ + { + status: 201, + }, + { + status: 201, + }, + ], + }, + status: 200, + }, + response: [ + { + statusCode: 200, + metadata: generateMetadata(1), + error: 'success', + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_ads/network.ts b/test/integrations/destinations/linkedin_ads/network.ts new file mode 100644 index 0000000000..890ad48589 --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/network.ts @@ -0,0 +1,186 @@ +export const headerBlockWithCorrectAccessToken = { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202402', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', +}; +export const element = { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '0', + currencyCode: 'USD', + }, + eventId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'abc@gmail.com', + }, + ], + }, +}; + +export const testJSONData = { + elements: [{ ...element }], +}; + +export const testJSONDataWithDifferentTypeConversion = { + elements: [ + { + ...element, + conversion: 'urn:li:partner:differentConversion', + }, + ], +}; + +export const wrongFormatElement = { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + currencyCode: 'USD', + }, + eventId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'abc@gmail.com', + }, + ], + userInfo: { + city: 'San Francisco', + }, + }, +}; + +export const wrongFormattedTestJSONData = { + elements: [{ ...wrongFormatElement }], +}; + +// MOCK DATA +const businessMockData = [ + { + description: 'Mock response from destination depicting request with a revoked access token', + httpReq: { + method: 'post', + url: 'https://api.linkedin.com/rest/conversionEvents', + headers: { ...headerBlockWithCorrectAccessToken, Authorization: 'Bearer revokedToken' }, + data: testJSONData, + }, + httpRes: { + data: { + status: 401, + serviceErrorCode: 65601, + code: 'REVOKED_ACCESS_TOKEN', + message: 'The token used in the request has been revoked by the user', + }, + status: 401, + statusText: 'OK', + }, + }, + { + description: 'Mock response from destination depicting request with an invalid access token', + httpReq: { + method: 'post', + url: 'https://api.linkedin.com/rest/conversionEvents', + headers: { ...headerBlockWithCorrectAccessToken, Authorization: 'Bearer invalidToken' }, + data: testJSONData, + }, + httpRes: { + data: { + status: 401, + serviceErrorCode: 65600, + code: 'INVALID_ACCESS_TOKEN', + message: 'Invalid access token', + }, + status: 401, + statusText: 'OK', + }, + }, + { + description: + 'Mock response from destination depicting a correct request with a valid access token', + httpReq: { + method: 'post', + url: 'https://api.linkedin.com/rest/conversionEvents', + headers: headerBlockWithCorrectAccessToken, + data: testJSONData, + }, + httpRes: { + data: { + elements: [ + { + status: 201, + }, + { + status: 201, + }, + ], + }, + status: 200, + statusText: 'OK', + }, + }, + { + description: + 'Mock response from destination depicting request with a conversion created differently than choosing direct API', + httpReq: { + method: 'post', + url: 'https://api.linkedin.com/rest/conversionEvents', + headers: headerBlockWithCorrectAccessToken, + data: testJSONDataWithDifferentTypeConversion, + }, + httpRes: { + data: { + message: + "Incorrect conversions information provided. Conversion's method should be CONVERSIONS_API, indices [0] (0-indexed)", + status: 400, + }, + status: 400, + statusText: 'OK', + }, + }, + { + description: + 'Mock response from destination depicting request with a conversion created differently than choosing direct API', + httpReq: { + method: 'post', + url: 'https://api.linkedin.com/rest/conversionEvents', + headers: headerBlockWithCorrectAccessToken, + data: testJSONDataWithDifferentTypeConversion, + }, + httpRes: { + data: { + message: + "Incorrect conversions information provided. Conversion's method should be CONVERSIONS_API, indices [0] (0-indexed)", + status: 400, + }, + status: 400, + statusText: 'OK', + }, + }, + { + description: + 'Mock response from destination depicting request with a conversion created differently than choosing direct API', + httpReq: { + method: 'post', + url: 'https://api.linkedin.com/rest/conversionEvents', + headers: headerBlockWithCorrectAccessToken, + data: wrongFormattedTestJSONData, + }, + httpRes: { + data: { + message: + 'Index: 0, ERROR :: /conversionValue/amount :: field is required but not found and has no default value\nERROR :: /user/userInfo/firstName :: field is required but not found and has no default value\nERROR :: /user/userInfo/lastName :: field is required but not found and has no default value\n', + status: 422, + }, + status: 422, + statusText: 'OK', + }, + }, +]; + +export const networkCallsData = [...businessMockData]; diff --git a/test/integrations/destinations/linkedin_ads/processor/configLevelFeaturesTestData.ts b/test/integrations/destinations/linkedin_ads/processor/configLevelFeaturesTestData.ts new file mode 100644 index 0000000000..287e35e5a7 --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/processor/configLevelFeaturesTestData.ts @@ -0,0 +1,219 @@ +import { + generateMetadata, + generateTrackPayload, + overrideDestination, + transformResultBuilder, +} from '../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const commonDestination: Destination = { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'linkedin_ads', + DisplayName: 'LinkedIn Ads', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + hashData: true, + conversionMapping: [ + { + from: 'ABC Searched', + to: '1234567', + }, + { + from: 'spin_result', + to: '23456', + }, + { + from: 'ABC Searched', + to: '34567', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + Enabled: true, +}; + +const commonUserTraits = { + email: 'abc@gmail.com', + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + event_id: '12345', +}; + +const commonUserProperties = { + price: 400, + additional_bet_index: 0, + eventId: '12345', +}; + +const commonTimestamp = new Date('2023-10-14'); + +const commonHeader = { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202402', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', +}; + +export const configLevelFeaturesTestData: ProcessorTestData[] = [ + { + id: 'linkedin_ads-config-test-1', + name: 'linkedin_ads', + description: 'Track call : hashData is set to false and no deduplication key is provided', + scenario: 'Business', + successCriteria: 'email provided will not be hashed and eventId will be mapped from messageId', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonUserProperties, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: overrideDestination(commonDestination, { hashData: false }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'abc@gmail.com', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-config-test-2', + name: 'linkedin_ads', + description: 'Track call : hashData is set to true and deduplication key is provided', + scenario: 'Business', + successCriteria: + 'email provided will be hashed and eventId will be mapped from deduplication key properties.eventId', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonUserProperties, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: overrideDestination(commonDestination, { + deduplicationKey: `properties.eventId`, + }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_ads/processor/data.ts b/test/integrations/destinations/linkedin_ads/processor/data.ts new file mode 100644 index 0000000000..edd6d1f1b5 --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/processor/data.ts @@ -0,0 +1,11 @@ +import { trackTestData } from './trackTestData'; +import { validationTestData } from './validationTestData'; +import { configLevelFeaturesTestData } from './configLevelFeaturesTestData'; + +export const mockFns = (_) => { + // @ts-ignore + jest.useFakeTimers().setSystemTime(new Date('2023-10-15')); +}; +export const data = [...trackTestData, ...validationTestData, ...configLevelFeaturesTestData].map( + (d) => ({ ...d, mockFns }), +); diff --git a/test/integrations/destinations/linkedin_ads/processor/trackTestData.ts b/test/integrations/destinations/linkedin_ads/processor/trackTestData.ts new file mode 100644 index 0000000000..f9dfc528db --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/processor/trackTestData.ts @@ -0,0 +1,718 @@ +import { generateMetadata, generateTrackPayload, transformResultBuilder } from '../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const commonDestination: Destination = { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'linkedin_ads', + DisplayName: 'LinkedIn Ads', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + hashData: true, + deduplicationKey: 'properties.eventId', + conversionMapping: [ + { + from: 'ABC Searched', + to: '1234567', + }, + { + from: 'spin_result', + to: '23456', + }, + { + from: 'ABC Searched', + to: '34567', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + Enabled: true, +}; + +const commonUserTraits = { + email: 'abc@gmail.com', + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + event_id: '12345', +}; + +const commonUserProperties = { + price: 400, + additional_bet_index: 0, + eventId: '12345', +}; + +const commonPropertiesWithProducts = { + revenue: 400, + additional_bet_index: 0, + eventId: '12345', + products: [ + { + product_id: '123', + name: 'abc', + category: 'def', + brand: 'xyz', + variant: 'pqr', + price: 100, + quantity: 2, + }, + { + product_id: '456', + name: 'def', + category: 'abc', + brand: 'pqr', + variant: 'xyz', + price: 200, + quantity: 3, + }, + ], +}; + +const commonPropertiesWithProductsPriceNotPresentInAll = { + revenue: 400, + additional_bet_index: 0, + eventId: '12345', + products: [ + { + product_id: '123', + name: 'abc', + category: 'def', + brand: 'xyz', + variant: 'pqr', + quantity: 2, + }, + { + product_id: '456', + name: 'def', + category: 'abc', + brand: 'pqr', + variant: 'xyz', + price: 200, + quantity: 3, + }, + ], +}; + +const commonTimestamp = new Date('2023-10-14'); + +const commonStatTags = { + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + implementation: 'cdkV2', + destType: 'LINKEDIN_ADS', + module: 'destination', + feature: 'processor', + workspaceId: 'default-workspaceId', +}; + +const commonHeader = { + Authorization: 'Bearer default-accessToken', + 'Content-Type': 'application/json', + 'LinkedIn-Version': '202402', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', +}; + +export const trackTestData: ProcessorTestData[] = [ + { + id: 'linkedin_ads-track-test-1', + name: 'linkedin_ads', + description: 'Track call : particular track event mapped to a specific conversion rule', + scenario: 'Business', + successCriteria: + 'event will respect the UI mapping and create a conversion event with the mapped conversion rule', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonUserProperties, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-track-test-2', + name: 'linkedin_ads', + description: 'Track call : event is mapped with more than one conversion rules ', + scenario: 'Business', + successCriteria: + 'event will respect the UI mapping and create a conversion event with the mapped conversion rule and club the two conversions in a single elements array', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'ABC Searched', + properties: commonUserProperties, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:1234567', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + }, + }, + { + conversion: 'urn:lla:llaPartnerConversion:34567', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-track-test-3', + name: 'linkedin_ads', + description: 'Track call : track event containing multiple allowed user identifiqers', + scenario: 'Business', + successCriteria: + 'event will respect the UI mapping and create a conversion event with the mapped conversion rule', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonUserProperties, + externalId: [ + { + id: 'test@rudderlabs.com', + type: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + }, + { + id: 'test@rudderlabs.com', + type: 'ACXIOM_ID', + }, + { + id: 'test@rudderlabs.com', + type: 'ORACLE_MOAT_ID', + }, + ], + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'test@rudderlabs.com', + }, + { + idType: 'ACXIOM_ID', + idValue: 'test@rudderlabs.com', + }, + { + idType: 'ORACLE_MOAT_ID', + idValue: 'test@rudderlabs.com', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-track-test-4', + name: 'linkedin_ads', + description: 'Track call : event not containing any of the allowed user identifiers', + scenario: 'Business', + successCriteria: + 'Error will be thrown as the event does not contain any of the allowed user identifiers', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'ABC Searched', + properties: commonUserProperties, + context: { + traits: { + firstName: 'John', + }, + }, + timestamp: commonTimestamp, + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '[LinkedIn Conversion API] no matching user id found. Please provide at least one of the following: email, linkedinFirstPartyAdsTrackingUUID, acxiomId, oracleMoatId: Workflow: procWorkflow, Step: commonFields, ChildStep: undefined, OriginalError: [LinkedIn Conversion API] no matching user id found. Please provide at least one of the following: email, linkedinFirstPartyAdsTrackingUUID, acxiomId, oracleMoatId', + metadata: generateMetadata(1), + statTags: commonStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-track-test-5', + name: 'linkedin_ads', + description: 'Track call : track event containing product array', + scenario: 'Business', + successCriteria: + 'the amount will be summation of product * quantity for all the products in the array', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonPropertiesWithProducts, + externalId: [ + { + id: 'test@rudderlabs.com', + type: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + }, + { + id: 'test@rudderlabs.com', + type: 'ACXIOM_ID', + }, + { + id: 'test@rudderlabs.com', + type: 'ORACLE_MOAT_ID', + }, + ], + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '800', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'test@rudderlabs.com', + }, + { + idType: 'ACXIOM_ID', + idValue: 'test@rudderlabs.com', + }, + { + idType: 'ORACLE_MOAT_ID', + idValue: 'test@rudderlabs.com', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-track-test-6', + name: 'linkedin_ads', + description: 'Track call : track event containing first name and last name in traits', + scenario: 'Business', + successCriteria: + 'output event will contain userInfo object only because first name and last name are present in traits', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonUserProperties, + context: { + traits: { ...commonUserTraits, firstName: 'John', lastName: 'Doe' }, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '400', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'John', + lastName: 'Doe', + }, + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-track-test-7', + name: 'linkedin_ads', + description: + 'Track call : track event containing product array where not all products contains price field', + scenario: 'Business', + successCriteria: + 'the amount will be summation of product * quantity for all the products in the array', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: commonPropertiesWithProductsPriceNotPresentInAll, + externalId: [ + { + id: 'test@rudderlabs.com', + type: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + }, + { + id: 'test@rudderlabs.com', + type: 'ACXIOM_ID', + }, + { + id: 'test@rudderlabs.com', + type: 'ORACLE_MOAT_ID', + }, + ], + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: commonDestination, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://api.linkedin.com/rest/conversionEvents`, + headers: commonHeader, + params: {}, + FORM: {}, + files: {}, + JSON: { + elements: [ + { + conversion: 'urn:lla:llaPartnerConversion:23456', + conversionHappenedAt: 1697241600000, + conversionValue: { + amount: '600', + currencyCode: 'USD', + }, + eventId: '12345', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'test@rudderlabs.com', + }, + { + idType: 'ACXIOM_ID', + idValue: 'test@rudderlabs.com', + }, + { + idType: 'ORACLE_MOAT_ID', + idValue: 'test@rudderlabs.com', + }, + ], + }, + }, + ], + }, + userId: '', + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_ads/processor/validationTestData.ts b/test/integrations/destinations/linkedin_ads/processor/validationTestData.ts new file mode 100644 index 0000000000..4579cf68ee --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/processor/validationTestData.ts @@ -0,0 +1,323 @@ +import { generateMetadata, generateTrackPayload, overrideDestination } from '../../../testUtils'; +import { Destination } from '../../../../../src/types'; +import { ProcessorTestData } from '../../../testTypes'; + +const commonDestination: Destination = { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'linkedin_ads', + DisplayName: 'LinkedIn Ads', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: { + hashData: true, + conversionMapping: [ + { + from: 'ABC Searched', + to: '1234567', + }, + { + from: 'spin_result', + to: '23456', + }, + { + from: 'ABC Searched', + to: '34567', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], + }, + Enabled: true, +}; + +const commonUserTraits = { + email: 'abc@gmail.com', + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + event_id: '12345', +}; + +const commonUserProperties = { + additional_bet_index: 0, + eventId: '12345', +}; + +const commonUserPropertiesWithProductWithoutPrice = { + additional_bet_index: 0, + eventId: '12345', + products: [ + { + productId: '12345', + }, + { + productId: '123456', + }, + ], +}; + +const commonStats = { + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + implementation: 'cdkV2', + destType: 'LINKEDIN_ADS', + module: 'destination', + feature: 'processor', + workspaceId: 'default-workspaceId', +}; + +const commonTimestamp = new Date('2023-10-14'); +const olderTimestamp = new Date('2023-07-13'); + +export const validationTestData: ProcessorTestData[] = [ + { + id: 'linkedin_ads-validation-test-1', + name: 'linkedin_ads', + description: 'Track call : event is older than 90 days', + scenario: 'Business', + successCriteria: 'shoud throw error with status code 400 and error message', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'spin_result', + properties: { ...commonUserProperties, price: 400 }, + context: { + traits: commonUserTraits, + }, + timestamp: olderTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: overrideDestination(commonDestination, { hashData: false }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Events must be sent within ninety days of their occurrence.: Workflow: procWorkflow, Step: commonFields, ChildStep: undefined, OriginalError: Events must be sent within ninety days of their occurrence.', + metadata: generateMetadata(1), + statTags: { + destinationId: 'default-destinationId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + implementation: 'cdkV2', + destType: 'LINKEDIN_ADS', + module: 'destination', + feature: 'processor', + workspaceId: 'default-workspaceId', + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-validation-test-2', + name: 'linkedin_ads', + description: 'Track call : event not mapped to conversion rule in UI', + scenario: 'Business', + successCriteria: + 'should throw error with status code 400 and error message no matching conversion rule found for random event. Please provide a conversion rule. Aborting', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'random event', + properties: { ...commonUserProperties, price: 400 }, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: overrideDestination(commonDestination, { + deduplicationKey: `properties.eventId`, + }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '[LinkedIn Conversion API] no matching conversion rule found for random event. Please provide a conversion rule. Aborting: Workflow: procWorkflow, Step: deduceConversionEventRules, ChildStep: undefined, OriginalError: [LinkedIn Conversion API] no matching conversion rule found for random event. Please provide a conversion rule. Aborting', + metadata: generateMetadata(1), + statTags: { ...commonStats, errorType: 'configuration' }, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-validation-test-3', + name: 'linkedin_ads', + description: '[Error]: Check for unsupported message type', + scenario: 'Framework', + successCriteria: + 'Response should contain error message and status code should be 400, as we are sending a message type which is not supported by linkedin_ads destination and the error message should be Event type random is not supported', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: commonDestination, + metadata: generateMetadata(1), + message: { + userId: 'user123', + type: 'random', + groupId: 'XUepkK', + traits: { + subscribe: true, + }, + context: { + traits: { + email: 'test@rudderstack.com', + phone: '+12 345 678 900', + consent: 'email', + }, + }, + timestamp: '2020-01-21T00:21:34.208Z', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'message type random is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: message type random is not supported', + metadata: generateMetadata(1), + statTags: commonStats, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-validation-test-4', + name: 'linkedin_ads', + description: 'Track call : properties without product array and no price', + scenario: 'Business', + successCriteria: + 'should throw error with status code 400 and error message regarding price is a mandatory field for linkedin conversions', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'random event', + properties: commonUserProperties, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: overrideDestination(commonDestination, { + deduplicationKey: `properties.eventId`, + }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '[LinkedIn Conversion API]: Cannot map price for event random event. Aborting: Workflow: procWorkflow, Step: commonFields, ChildStep: undefined, OriginalError: [LinkedIn Conversion API]: Cannot map price for event random event. Aborting', + metadata: generateMetadata(1), + statTags: commonStats, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'linkedin_ads-validation-test-5', + name: 'linkedin_ads', + description: 'Track call : properties with product array and no price', + scenario: 'Business', + successCriteria: + 'should throw error with status code 400 and error message regarding price is a mandatory field for linkedin conversions', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: generateTrackPayload({ + event: 'random event', + properties: commonUserPropertiesWithProductWithoutPrice, + context: { + traits: commonUserTraits, + }, + timestamp: commonTimestamp, + messageId: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + }), + metadata: generateMetadata(1), + destination: overrideDestination(commonDestination, { + deduplicationKey: `properties.eventId`, + }), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '[LinkedIn Conversion API]: Cannot map price for event random event. Aborting: Workflow: procWorkflow, Step: commonFields, ChildStep: undefined, OriginalError: [LinkedIn Conversion API]: Cannot map price for event random event. Aborting', + metadata: generateMetadata(1), + statTags: commonStats, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/linkedin_ads/router/data.ts b/test/integrations/destinations/linkedin_ads/router/data.ts new file mode 100644 index 0000000000..cf7defe6af --- /dev/null +++ b/test/integrations/destinations/linkedin_ads/router/data.ts @@ -0,0 +1,462 @@ +export const mockFns = (_) => { + // @ts-ignore + jest.useFakeTimers().setSystemTime(new Date('2023-10-15')); +}; + +const config = { + hashData: true, + deduplicationKey: 'properties.eventId', + conversionMapping: [ + { + from: 'ABC Searched', + to: '1234567', + }, + { + from: 'spin_result', + to: '23456', + }, + { + from: 'ABC Searched', + to: '34567', + }, + ], + oneTrustCookieCategories: [ + { + oneTrustCookieCategory: 'Marketing', + }, + ], +}; + +const commonDestination = { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'linkedin_ads', + DisplayName: 'LinkedIn Ads', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: config, + Enabled: true, +}; + +export const data = [ + { + id: 'linkedin_ads-track-test-1', + name: 'linkedin_ads', + description: 'Track call : custom event calls with simple user properties and traits', + scenario: 'Business', + successCriteria: + 'event not respecting the internal mapping and as well as UI mapping should be considered as a custom event and should be sent as it is', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'track', + event: 'ABC Searched', + sentAt: '2020-08-14T05: 30: 30.118Z', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + email: 'abc@gmail.com', + phone: '+1234589947', + gender: 'non-binary', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2024-02-10T12:16:07.251Z', + properties: { + tax: 2, + total: 27.5, + coupon: 'hasbros', + revenue: 48, + price: 25, + quantity: 2, + currency: 'USD', + discount: 2.5, + order_id: '50314b8e9bcf000000000000', + requestIP: '123.0.0.0', + optOutType: 'LDP', + clickId: 'dummy_clickId', + + shipping: 3, + subtotal: 22.5, + affiliation: 'Google Store', + checkout_id: 'fksdjfsdjfisjf9sdfjsd9f', + }, + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 1, + secret: { + accessToken: 'dummyToken', + }, + }, + destination: commonDestination, + }, + { + message: { + type: 'track', + event: 'ABC Searched', + sentAt: '2020-08-14T05: 30: 30.118Z', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + email: 'abc@gmail.com', + phone: '+1234589947', + gender: 'non-binary', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2024-02-10T12:16:07.251Z', + properties: { + tax: 2, + total: 27.5, + coupon: 'hasbros', + revenue: 48, + price: 25, + quantity: 2, + currency: 'USD', + discount: 2.5, + order_id: '50314b8e9bcf000000000000', + requestIP: '123.0.0.0', + optOutType: 'LDP', + clickId: 'dummy_clickId', + + shipping: 3, + subtotal: 22.5, + affiliation: 'Google Store', + checkout_id: 'fksdjfsdjfisjf9sdfjsd9f', + }, + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 2, + secret: { + accessToken: 'dummyToken', + }, + }, + destination: commonDestination, + }, + { + message: { + type: 'track', + event: 'spin_result', + sentAt: '2020-08-14T05: 30: 30.118Z', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + phone: '+1234589947', + gender: 'non-binary', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2024-02-10T12:16:07.251Z', + properties: { + tax: 2, + total: 27.5, + coupon: 'hasbros', + revenue: 48, + price: 25, + quantity: 2, + currency: 'USD', + discount: 2.5, + order_id: '50314b8e9bcf000000000000', + requestIP: '123.0.0.0', + optOutType: 'LDP', + clickId: 'dummy_clickId', + + shipping: 3, + subtotal: 22.5, + affiliation: 'Google Store', + checkout_id: 'fksdjfsdjfisjf9sdfjsd9f', + }, + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 3, + secret: { + accessToken: 'dummyToken', + }, + }, + destination: commonDestination, + }, + ], + destType: 'linkedin_ads', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 3, + secret: { + accessToken: 'dummyToken', + }, + }, + ], + destination: { + ID: '12335', + Name: 'sample-destination', + DestinationDefinition: { + ID: '123', + Name: 'linkedin_ads', + DisplayName: 'LinkedIn Ads', + Config: { + cdkV2Enabled: true, + }, + }, + WorkspaceID: '123', + Transformations: [], + Config: config, + Enabled: true, + }, + batched: false, + statusCode: 400, + error: + '[LinkedIn Conversion API] no matching user id found. Please provide at least one of the following: email, linkedinFirstPartyAdsTrackingUUID, acxiomId, oracleMoatId', + statTags: { + destType: 'LINKEDIN_ADS', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', + }, + }, + { + batchedRequest: { + body: { + JSON: { + elements: [ + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:1234567', + }, + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:34567', + }, + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:1234567', + }, + { + conversionHappenedAt: 1707567367251, + eventId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + conversionValue: { + currencyCode: 'USD', + amount: '50', + }, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: + '48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08', + }, + ], + userInfo: { + firstName: 'Test', + lastName: 'Rudderlabs', + }, + }, + conversion: 'urn:lla:llaPartnerConversion:34567', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.linkedin.com/rest/conversionEvents', + headers: { + 'Content-Type': 'application/json', + 'X-RestLi-Method': 'BATCH_CREATE', + 'X-Restli-Protocol-Version': '2.0.0', + 'LinkedIn-Version': '202402', + Authorization: 'Bearer dummyToken', + }, + params: {}, + files: {}, + }, + metadata: [ + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 1, + secret: { + accessToken: 'dummyToken', + }, + }, + { + sourceType: '', + destinationType: '', + namespace: '', + jobId: 2, + secret: { + accessToken: 'dummyToken', + }, + }, + ], + batched: true, + statusCode: 200, + destination: commonDestination, + }, + ], + }, + }, + }, + }, +].map((d) => ({ ...d, mockFns })); diff --git a/test/integrations/destinations/mailjet/processor/data.ts b/test/integrations/destinations/mailjet/processor/data.ts index 71e06dc14e..03c3232e72 100644 --- a/test/integrations/destinations/mailjet/processor/data.ts +++ b/test/integrations/destinations/mailjet/processor/data.ts @@ -141,7 +141,7 @@ export const data = [ status: 200, body: [ { - error: 'Missing required value from "email"', + error: 'Missing required value from "emailOnly"', statTags: { destType: 'MAILJET', errorCategory: 'dataValidation', diff --git a/test/integrations/destinations/monday/processor/data.ts b/test/integrations/destinations/monday/processor/data.ts index 4e5280efcb..082ff822fd 100644 --- a/test/integrations/destinations/monday/processor/data.ts +++ b/test/integrations/destinations/monday/processor/data.ts @@ -74,7 +74,7 @@ export const data = [ FORM: {}, JSON: { query: - 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{}") {id}}', }, JSON_ARRAY: {}, XML: {}, @@ -172,7 +172,7 @@ export const data = [ FORM: {}, JSON: { query: - 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{}") {id}}', }, JSON_ARRAY: {}, XML: {}, @@ -716,7 +716,7 @@ export const data = [ body: { JSON: { query: - 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"}}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"}}") {id}}', }, JSON_ARRAY: {}, XML: {}, @@ -827,7 +827,7 @@ export const data = [ body: { JSON: { query: - 'mutation { create_item (board_id: 339283933, group_id: group_title item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"}}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"}}", group_id: "group_title") {id}}', }, JSON_ARRAY: {}, XML: {}, @@ -1188,7 +1188,7 @@ export const data = [ body: { JSON: { query: - 'mutation { create_item (board_id: 339283933, group_id: group_title item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"},\\"checkbox\\":{\\"checked\\":true},\\"numbers\\":\\"45\\",\\"text\\":\\"texting\\",\\"country\\":{\\"countryName\\":\\"Unites States\\",\\"countryCode\\":\\"US\\"},\\"location\\":{\\"address\\":\\"New York\\",\\"lat\\":\\"51.23\\",\\"lng\\":\\"35.3\\"},\\"phone\\":{\\"phone\\":\\"2626277272\\",\\"countryShortName\\":\\"US\\"},\\"rating\\":3,\\"link\\":{\\"url\\":\\"demo.com\\",\\"text\\":\\"websiteLink\\"},\\"long_text\\":{\\"text\\":\\"property description\\"},\\"world_clock\\":{\\"timezone\\":\\"America/New_York\\"}}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"},\\"checkbox\\":{\\"checked\\":true},\\"numbers\\":\\"45\\",\\"text\\":\\"texting\\",\\"country\\":{\\"countryName\\":\\"Unites States\\",\\"countryCode\\":\\"US\\"},\\"location\\":{\\"address\\":\\"New York\\",\\"lat\\":\\"51.23\\",\\"lng\\":\\"35.3\\"},\\"phone\\":{\\"phone\\":\\"2626277272\\",\\"countryShortName\\":\\"US\\"},\\"rating\\":3,\\"link\\":{\\"url\\":\\"demo.com\\",\\"text\\":\\"websiteLink\\"},\\"long_text\\":{\\"text\\":\\"property description\\"},\\"world_clock\\":{\\"timezone\\":\\"America/New_York\\"}}", group_id: "group_title") {id}}', }, JSON_ARRAY: {}, XML: {}, diff --git a/test/integrations/destinations/monday/router/data.ts b/test/integrations/destinations/monday/router/data.ts index 3be8b129c5..abd649d805 100644 --- a/test/integrations/destinations/monday/router/data.ts +++ b/test/integrations/destinations/monday/router/data.ts @@ -113,7 +113,7 @@ export const data = [ FORM: {}, JSON: { query: - 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{}") {id}}', }, JSON_ARRAY: {}, XML: {}, @@ -159,7 +159,7 @@ export const data = [ body: { JSON: { query: - 'mutation { create_item (board_id: 339283933, group_id: group_title item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"}}") {id}}', + 'mutation { create_item (board_id: 339283933, item_name: "Task 1", column_values: "{\\"status\\":{\\"label\\":\\"Done\\"},\\"email\\":{\\"email\\":\\"abc@email.com\\",\\"text\\":\\"emailId\\"}}", group_id: "group_title") {id}}', }, JSON_ARRAY: {}, XML: {}, diff --git a/test/integrations/destinations/movable_ink/common.ts b/test/integrations/destinations/movable_ink/common.ts index f7eaa7af39..29fe76852c 100644 --- a/test/integrations/destinations/movable_ink/common.ts +++ b/test/integrations/destinations/movable_ink/common.ts @@ -110,6 +110,186 @@ const trackTestProperties = { position: 2, category: 'Games', }, + { + product_id: '122c6f5d5cf86a4c77358033', + sku: '7472-998-0112', + name: 'Ticket to Ride', + price: 20, + position: 3, + category: 'Games', + }, + { + product_id: '222c6f5d5cf86a4c77358033', + sku: '9472-998-0112', + name: 'Catan', + price: 30, + position: 4, + category: 'Games', + }, + { + product_id: '322c6f5d5cf86a4c77358033', + sku: '7472-998-0112', + name: 'Pandemic', + price: 25, + position: 5, + category: 'Games', + }, + { + product_id: '422c6f5d5cf86a4c77358033', + sku: '8472-998-0113', + name: 'Exploding Kittens', + price: 15, + position: 6, + category: 'Games', + }, + { + product_id: '522c6f5d5cf86a4c77358033', + sku: '8472-998-0114', + name: 'Codenames', + price: 18, + position: 7, + category: 'Games', + }, + { + product_id: '622c6f5d5cf86a4c77358034', + sku: '8472-998-0115', + name: 'Scythe', + price: 35, + position: 8, + category: 'Games', + }, + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + name: 'Cones of Dunshire', + price: 40, + position: 1, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + name: 'Five Crowns', + price: 5, + position: 2, + category: 'Games', + }, + { + product_id: '122c6f5d5cf86a4c77358033', + sku: '7472-998-0112', + name: 'Ticket to Ride', + price: 20, + position: 3, + category: 'Games', + }, + { + product_id: '222c6f5d5cf86a4c77358033', + sku: '9472-998-0112', + name: 'Catan', + price: 30, + position: 4, + category: 'Games', + }, + { + product_id: '322c6f5d5cf86a4c77358033', + sku: '7472-998-0112', + name: 'Pandemic', + price: 25, + position: 5, + category: 'Games', + }, + { + product_id: '422c6f5d5cf86a4c77358033', + sku: '8472-998-0113', + name: 'Exploding Kittens', + price: 15, + position: 6, + category: 'Games', + }, + { + product_id: '522c6f5d5cf86a4c77358033', + sku: '8472-998-0114', + name: 'Codenames', + price: 18, + position: 7, + category: 'Games', + }, + { + product_id: '622c6f5d5cf86a4c77358034', + sku: '8472-998-0115', + name: 'Scythe', + price: 35, + position: 8, + category: 'Games', + }, + { + product_id: '622c6f5d5cf86a4c77358033', + sku: '8472-998-0112', + name: 'Cones of Dunshire', + price: 40, + position: 1, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { + product_id: '577c6f5d5cf86a4c7735ba03', + sku: '3309-483-2201', + name: 'Five Crowns', + price: 5, + position: 2, + category: 'Games', + }, + { + product_id: '122c6f5d5cf86a4c77358033', + sku: '7472-998-0112', + name: 'Ticket to Ride', + price: 20, + position: 3, + category: 'Games', + }, + { + product_id: '222c6f5d5cf86a4c77358033', + sku: '9472-998-0112', + name: 'Catan', + price: 30, + position: 4, + category: 'Games', + }, + { + product_id: '322c6f5d5cf86a4c77358033', + sku: '7472-998-0112', + name: 'Pandemic', + price: 25, + position: 5, + category: 'Games', + }, + { + product_id: '422c6f5d5cf86a4c77358033', + sku: '8472-998-0113', + name: 'Exploding Kittens', + price: 15, + position: 6, + category: 'Games', + }, + { + product_id: '522c6f5d5cf86a4c77358033', + sku: '8472-998-0114', + name: 'Codenames', + price: 18, + position: 7, + category: 'Games', + }, + { + product_id: '622c6f5d5cf86a4c77358034', + sku: '8472-998-0115', + name: 'Scythe', + price: 35, + position: 8, + category: 'Games', + }, ], }, 'Products Searched': { query: 'HDMI cable', url: 'https://www.website.com/product/path' }, diff --git a/test/integrations/destinations/movable_ink/mocks.ts b/test/integrations/destinations/movable_ink/mocks.ts new file mode 100644 index 0000000000..2468f51315 --- /dev/null +++ b/test/integrations/destinations/movable_ink/mocks.ts @@ -0,0 +1,6 @@ +import config from '../../../../src/cdk/v2/destinations/movable_ink/config'; + +export const defaultMockFns = () => { + jest.replaceProperty(config, 'MAX_REQUEST_SIZE_IN_BYTES', 5000); + jest.replaceProperty(config, 'MAX_BATCH_SIZE', 2); +}; diff --git a/test/integrations/destinations/movable_ink/processor/identify.ts b/test/integrations/destinations/movable_ink/processor/identify.ts index 27186da05c..e5bbf5a9a7 100644 --- a/test/integrations/destinations/movable_ink/processor/identify.ts +++ b/test/integrations/destinations/movable_ink/processor/identify.ts @@ -1,6 +1,6 @@ import { ProcessorTestData } from '../../../testTypes'; import { generateMetadata, transformResultBuilder } from '../../../testUtils'; -import { destType, channel, destination, traits, headers } from '../common'; +import { destType, destination, traits, headers } from '../common'; export const identify: ProcessorTestData[] = [ { diff --git a/test/integrations/destinations/movable_ink/processor/track.ts b/test/integrations/destinations/movable_ink/processor/track.ts index 5f30a3de83..890de11a0c 100644 --- a/test/integrations/destinations/movable_ink/processor/track.ts +++ b/test/integrations/destinations/movable_ink/processor/track.ts @@ -23,6 +23,7 @@ export const track: ProcessorTestData[] = [ channel, anonymousId: 'anonId123', userId: 'userId123', + event: 'Product Added', properties: trackTestProperties['Product Added'], integrations: { All: true, @@ -49,6 +50,7 @@ export const track: ProcessorTestData[] = [ channel, userId: 'userId123', anonymousId: 'anonId123', + event: 'Product Added', properties: trackTestProperties['Product Added'], integrations: { All: true, @@ -84,6 +86,7 @@ export const track: ProcessorTestData[] = [ channel, anonymousId: 'anonId123', userId: 'userId123', + event: 'Order Completed', properties: trackTestProperties['Order Completed'], integrations: { All: true, @@ -110,6 +113,7 @@ export const track: ProcessorTestData[] = [ channel, userId: 'userId123', anonymousId: 'anonId123', + event: 'Order Completed', properties: trackTestProperties['Order Completed'], integrations: { All: true, @@ -145,6 +149,7 @@ export const track: ProcessorTestData[] = [ channel, anonymousId: 'anonId123', userId: 'userId123', + event: 'Custom Event', properties: trackTestProperties['Custom Event'], integrations: { All: true, @@ -171,6 +176,7 @@ export const track: ProcessorTestData[] = [ channel, userId: 'userId123', anonymousId: 'anonId123', + event: 'Custom Event', properties: trackTestProperties['Custom Event'], integrations: { All: true, diff --git a/test/integrations/destinations/movable_ink/processor/validation.ts b/test/integrations/destinations/movable_ink/processor/validation.ts index ab6b123eb7..6aafb5e2c0 100644 --- a/test/integrations/destinations/movable_ink/processor/validation.ts +++ b/test/integrations/destinations/movable_ink/processor/validation.ts @@ -214,4 +214,48 @@ export const validation: ProcessorTestData[] = [ }, }, }, + { + id: 'MovableInk-validation-test-6', + name: destType, + description: 'Missing event name', + scenario: 'Framework', + successCriteria: 'Instrumentation Error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + anonymousId: 'anonId123', + userId: 'userId123', + properties: {}, + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Event name is not present. Aborting: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Event name is not present. Aborting', + metadata: generateMetadata(1), + statTags: processorInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/movable_ink/router/data.ts b/test/integrations/destinations/movable_ink/router/data.ts index 72df3d7074..afadfec56e 100644 --- a/test/integrations/destinations/movable_ink/router/data.ts +++ b/test/integrations/destinations/movable_ink/router/data.ts @@ -1,6 +1,7 @@ import { RouterTestData } from '../../../testTypes'; import { RouterTransformationRequest } from '../../../../../src/types'; import { generateMetadata } from '../../../testUtils'; +import { defaultMockFns } from '../mocks'; import { destType, channel, @@ -43,6 +44,7 @@ const routerRequest: RouterTransformationRequest = { channel, anonymousId: 'anonId123', userId: 'userId123', + event: 'Product Added', properties: trackTestProperties['Product Added'], integrations: { All: true, @@ -58,19 +60,73 @@ const routerRequest: RouterTransformationRequest = { channel, anonymousId: 'anonId123', userId: 'userId123', - properties: trackTestProperties['Custom Event'], + event: 'Custom Event', integrations: { All: true, }, + originalTimestamp: '2024-03-04T15:32:56.409Z', }, metadata: generateMetadata(4), destination, }, + { + message: { + type: 'track', + channel, + anonymousId: 'anonId123', + userId: 'userId123', + event: 'Custom Event', + properties: trackTestProperties['Custom Event'], + integrations: { + All: true, + }, + }, + metadata: generateMetadata(5), + destination, + }, + ], + destType, +}; + +// >5KB payload +const routerRequest2: RouterTransformationRequest = { + input: [ + { + message: { + type: 'track', + channel, + anonymousId: 'anonId123', + userId: 'userId123', + event: 'Order Completed', + properties: trackTestProperties['Order Completed'], + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(1), + destination, + }, + { + message: { + type: 'track', + channel, + anonymousId: 'anonId123', + userId: 'userId123', + event: 'Custom Event', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + }, + metadata: generateMetadata(2), + destination, + }, ], destType, }; -export const data: RouterTestData[] = [ +export const data = [ { id: 'MovableInk-router-test-1', name: destType, @@ -118,6 +174,7 @@ export const data: RouterTestData[] = [ channel, userId: 'userId123', anonymousId: 'anonId123', + event: 'Product Added', properties: trackTestProperties['Product Added'], integrations: { All: true, @@ -138,6 +195,42 @@ export const data: RouterTestData[] = [ statusCode: 200, destination, }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: destination.Config.endpoint, + headers, + params: {}, + body: { + JSON: { + events: [ + { + type: 'track', + channel, + userId: 'userId123', + anonymousId: 'anonId123', + event: 'Custom Event', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + timestamp: 1709566376409, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(4)], + batched: true, + statusCode: 200, + destination, + }, { metadata: [generateMetadata(2)], batched: false, @@ -147,7 +240,7 @@ export const data: RouterTestData[] = [ destination, }, { - metadata: [generateMetadata(4)], + metadata: [generateMetadata(5)], batched: false, statusCode: 400, error: 'Timestamp is not present. Aborting', @@ -158,5 +251,105 @@ export const data: RouterTestData[] = [ }, }, }, + mockFns: defaultMockFns, + }, + { + id: 'MovableInk-router-test-2', + name: destType, + description: 'Basic Router Test to test Max Request Size', + scenario: 'Framework', + successCriteria: + 'Some events should be transformed successfully and some should fail for missing fields and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest2, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: destination.Config.endpoint, + headers, + params: {}, + body: { + JSON: { + events: [ + { + type: 'track', + channel, + userId: 'userId123', + anonymousId: 'anonId123', + event: 'Order Completed', + properties: trackTestProperties['Order Completed'], + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + timestamp: 1709566376409, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: destination.Config.endpoint, + headers, + params: {}, + body: { + JSON: { + events: [ + { + type: 'track', + channel, + userId: 'userId123', + anonymousId: 'anonId123', + event: 'Custom Event', + integrations: { + All: true, + }, + originalTimestamp: '2024-03-04T15:32:56.409Z', + timestamp: 1709566376409, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(2)], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, }, ]; diff --git a/test/integrations/destinations/mp/processor/data.ts b/test/integrations/destinations/mp/processor/data.ts index 5b2d0fbfff..2d70d15384 100644 --- a/test/integrations/destinations/mp/processor/data.ts +++ b/test/integrations/destinations/mp/processor/data.ts @@ -4297,7 +4297,10 @@ export const data = [ Authorization: 'Basic cnVkZGVyLmQyYTNmMS5tcC1zZXJ2aWNlLWFjY291bnQ6amF0cFF4Y2pNaDhlZXRrMXhySDNLalFJYnp5NGlYOGI=', }, - params: { project_id: '123456', strict: 0 }, + params: { + project_id: '123456', + strict: 0, + }, body: { JSON: {}, JSON_ARRAY: { @@ -4622,7 +4625,7 @@ export const data = [ description: 'Track: set device id and user id when simplified id merge api is selected', destination: overrideDestination(sampleDestination, { - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', }), message: { @@ -4685,7 +4688,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"event":"Product Viewed","properties":{"name":"T-Shirt","$user_id":"userId01","$os":"iOS","$screen_height":1794,"$screen_width":1080,"$screen_dpi":420,"$carrier":"Android","$os_version":"8.1.0","$device":"generic_x86","$manufacturer":"Google","$model":"Android SDK built for x86","mp_device_model":"Android SDK built for x86","$wifi":true,"$bluetooth_enabled":false,"mp_lib":"com.rudderstack.android.sdk.core","$app_build_number":"1","$app_version_string":"1.0","$insert_id":"id2","token":"apiToken123","distinct_id":"userId01","time":1579847342402,"$device_id":"anonId01"}}]', + '[{"event":"Product Viewed","properties":{"name":"T-Shirt","$user_id":"userId01","$os":"iOS","$screen_height":1794,"$screen_width":1080,"$screen_dpi":420,"$carrier":"Android","$os_version":"8.1.0","$device":"generic_x86","$manufacturer":"Google","$model":"Android SDK built for x86","mp_device_model":"Android SDK built for x86","$wifi":true,"$bluetooth_enabled":false,"mp_lib":"com.rudderstack.android.sdk.core","$app_build_number":"1","$app_version_string":"1.0","$insert_id":"id2","token":"dummyApiKey","distinct_id":"userId01","time":1579847342402,"$device_id":"anonId01"}}]', }, XML: {}, FORM: {}, @@ -4714,7 +4717,7 @@ export const data = [ { description: 'Identify: skip merge event when simplified id merge api is selected', destination: overrideDestination(sampleDestination, { - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', }), message: { @@ -4795,7 +4798,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"$set":{"$created":"2020-01-23T08:54:02.362Z","$email":"mickey@disney.com","$country_code":"USA","$city":"Disney","$initial_referrer":"https://docs.rudderstack.com","$initial_referring_domain":"docs.rudderstack.com","$name":"Mickey Mouse","$firstName":"Mickey","$lastName":"Mouse","$browser":"Chrome","$browser_version":"79.0.3945.117"},"$token":"apiToken123","$distinct_id":"userId01","$ip":"0.0.0.0","$time":1579847342402}]', + '[{"$set":{"$created":"2020-01-23T08:54:02.362Z","$email":"mickey@disney.com","$country_code":"USA","$city":"Disney","$initial_referrer":"https://docs.rudderstack.com","$initial_referring_domain":"docs.rudderstack.com","$name":"Mickey Mouse","$firstName":"Mickey","$lastName":"Mouse","$browser":"Chrome","$browser_version":"79.0.3945.117"},"$token":"dummyApiKey","$distinct_id":"userId01","$ip":"0.0.0.0","$time":1579847342402}]', }, XML: {}, FORM: {}, @@ -4823,7 +4826,7 @@ export const data = [ 'Identify: append $device: to deviceId while creating the user when simplified id merge api is selected', destination: overrideDestination(sampleDestination, { apiKey: 'apiKey123', - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', }), message: { @@ -4903,7 +4906,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"$set":{"$created":"2020-01-23T08:54:02.362Z","$email":"mickey@disney.com","$country_code":"USA","$city":"Disney","$initial_referrer":"https://docs.rudderstack.com","$initial_referring_domain":"docs.rudderstack.com","$name":"Mickey Mouse","$firstName":"Mickey","$lastName":"Mouse","$browser":"Chrome","$browser_version":"79.0.3945.117"},"$token":"apiToken123","$distinct_id":"$device:anonId01","$ip":"0.0.0.0","$time":1579847342402}]', + '[{"$set":{"$created":"2020-01-23T08:54:02.362Z","$email":"mickey@disney.com","$country_code":"USA","$city":"Disney","$initial_referrer":"https://docs.rudderstack.com","$initial_referring_domain":"docs.rudderstack.com","$name":"Mickey Mouse","$firstName":"Mickey","$lastName":"Mouse","$browser":"Chrome","$browser_version":"79.0.3945.117"},"$token":"dummyApiKey","$distinct_id":"$device:anonId01","$ip":"0.0.0.0","$time":1579847342402}]', }, XML: {}, FORM: {}, @@ -4930,7 +4933,7 @@ export const data = [ description: 'Unsupported alias call when simplified id merge api is selected', destination: overrideDestination(sampleDestination, { apiKey: 'apiKey123', - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', }), message: { @@ -5019,7 +5022,7 @@ export const data = [ 'Track revenue event: set device id and user id when simplified id merge api is selected', destination: overrideDestination(sampleDestination, { apiKey: 'apiKey123', - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', }), message: { @@ -5090,7 +5093,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"$append":{"$transactions":{"$time":"2020-01-24T06:29:02.403Z","$amount":18.9}},"$token":"apiToken123","$distinct_id":"userId01"}]', + '[{"$append":{"$transactions":{"$time":"2020-01-24T06:29:02.403Z","$amount":18.9}},"$token":"dummyApiKey","$distinct_id":"userId01"}]', }, XML: {}, FORM: {}, @@ -5112,7 +5115,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"event":"test revenue MIXPANEL","properties":{"currency":"USD","revenue":18.9,"city":"Disney","country":"USA","email":"mickey@disney.com","firstName":"Mickey","ip":"0.0.0.0","$user_id":"userId01","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"a6a0ad5a-bd26-4f19-8f75-38484e580fc7","token":"apiToken123","distinct_id":"userId01","time":1579847342403,"$device_id":"anonId01","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', + '[{"event":"test revenue MIXPANEL","properties":{"currency":"USD","revenue":18.9,"city":"Disney","country":"USA","email":"mickey@disney.com","firstName":"Mickey","ip":"0.0.0.0","$user_id":"userId01","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"a6a0ad5a-bd26-4f19-8f75-38484e580fc7","token":"dummyApiKey","distinct_id":"userId01","time":1579847342403,"$device_id":"anonId01","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', }, XML: {}, FORM: {}, @@ -5142,7 +5145,7 @@ export const data = [ description: 'Page with anonymous user when simplified api is selected', destination: overrideDestination(sampleDestination, { apiKey: 'apiKey123', - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', }), message: { @@ -5216,7 +5219,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"event":"Loaded a Page","properties":{"ip":"0.0.0.0","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"dd266c67-9199-4a52-ba32-f46ddde67312","token":"apiToken123","distinct_id":"$device:anonId01","time":1579847342402,"$device_id":"anonId01","name":"Contact Us","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', + '[{"event":"Loaded a Page","properties":{"ip":"0.0.0.0","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"dd266c67-9199-4a52-ba32-f46ddde67312","token":"dummyApiKey","distinct_id":"$device:anonId01","time":1579847342402,"$device_id":"anonId01","name":"Contact Us","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', }, XML: {}, FORM: {}, @@ -5246,7 +5249,7 @@ export const data = [ description: 'Group call with anonymous user when simplified api is selected', destination: overrideDestination(sampleDestination, { apiKey: 'apiKey123', - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', groupKeySettings: [{ groupKey: 'company' }], }), @@ -5309,7 +5312,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"$token":"apiToken123","$distinct_id":"$device:anonId01","$set":{"company":["testComp"]},"$ip":"0.0.0.0"}]', + '[{"$token":"dummyApiKey","$distinct_id":"$device:anonId01","$set":{"company":["testComp"]},"$ip":"0.0.0.0"}]', }, XML: {}, FORM: {}, @@ -5331,7 +5334,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"$token":"apiToken123","$group_key":"company","$group_id":"testComp","$set":{"company":"testComp"}}]', + '[{"$token":"dummyApiKey","$group_key":"company","$group_id":"testComp","$set":{"company":"testComp"}}]', }, XML: {}, FORM: {}, @@ -5357,7 +5360,7 @@ export const data = [ { destination: overrideDestination(sampleDestination, { apiKey: 'apiKey123', - token: 'apiToken123', + token: 'dummyApiKey', identityMergeApi: 'simplified', groupKeySettings: [{ groupKey: 'company' }], }), diff --git a/test/integrations/destinations/ninetailed/processor/data.ts b/test/integrations/destinations/ninetailed/processor/data.ts index 4e5fa72365..9d3cd217cd 100644 --- a/test/integrations/destinations/ninetailed/processor/data.ts +++ b/test/integrations/destinations/ninetailed/processor/data.ts @@ -1,5 +1,4 @@ import { validationFailures } from './validation'; import { track } from './track'; -import { page } from './page'; import { identify } from './identify'; -export const data = [...identify, ...page, ...track, ...validationFailures]; +export const data = [...identify, ...track, ...validationFailures]; diff --git a/test/integrations/destinations/ninetailed/processor/page.ts b/test/integrations/destinations/ninetailed/processor/page.ts deleted file mode 100644 index 93a086ceea..0000000000 --- a/test/integrations/destinations/ninetailed/processor/page.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { destination, context, commonProperties, metadata } from '../commonConfig'; -import { transformResultBuilder } from '../../../testUtils'; -export const page = [ - { - id: 'ninetailed-test-page-success-1', - name: 'ninetailed', - description: 'page call with all mappings available', - scenario: 'Framework+Buisness', - successCriteria: 'Response should contain all the mappings and status code should be 200', - feature: 'processor', - module: 'destination', - version: 'v0', - input: { - request: { - body: [ - { - destination, - message: { - context, - type: 'page', - event: 'product purchased', - userId: 'sajal12', - channel: 'mobile', - messageId: 'dummy_msg_id', - properties: commonProperties, - anonymousId: 'anon_123', - integrations: { - All: true, - }, - originalTimestamp: '2021-01-25T15:32:56.409Z', - }, - metadata, - }, - ], - }, - }, - output: { - response: { - status: 200, - body: [ - { - metadata: { - destinationId: 'dummyDestId', - }, - output: transformResultBuilder({ - method: 'POST', - endpoint: - 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', - JSON: { - events: [ - { - context: { - app: { - name: 'RudderLabs JavaScript SDK', - version: '1.0.0', - }, - campaign: { - name: 'campign_123', - source: 'social marketing', - medium: 'facebook', - term: '1 year', - }, - library: { - name: 'RudderstackSDK', - version: 'Ruddderstack SDK version', - }, - locale: 'en-US', - page: { - path: '/signup', - referrer: 'https://rudderstack.medium.com/', - search: '?type=freetrial', - url: 'https://app.rudderstack.com/signup?type=freetrial', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', - location: { - coordinates: { - latitude: 40.7128, - longitude: -74.006, - }, - city: 'San Francisco', - postalCode: '94107', - region: 'CA', - regionCode: 'CA', - country: ' United States', - countryCode: 'United States of America', - continent: 'North America', - timezone: 'America/Los_Angeles', - }, - }, - type: 'page', - channel: 'mobile', - messageId: 'dummy_msg_id', - properties: commonProperties, - anonymousId: 'anon_123', - originalTimestamp: '2021-01-25T15:32:56.409Z', - }, - ], - }, - userId: '', - }), - statusCode: 200, - }, - ], - }, - }, - }, -]; diff --git a/test/integrations/destinations/ninetailed/router/data.ts b/test/integrations/destinations/ninetailed/router/data.ts index 05105f4aed..1bf664d1c4 100644 --- a/test/integrations/destinations/ninetailed/router/data.ts +++ b/test/integrations/destinations/ninetailed/router/data.ts @@ -31,15 +31,6 @@ export const data = [ metadata: { jobId: 1, userId: 'u1' }, destination, }, - { - message: { - ...commonInput, - type: 'page', - properties: pageProperties, - }, - metadata: { jobId: 2, userId: 'u1' }, - destination, - }, { message: { type: 'identify', @@ -80,11 +71,6 @@ export const data = [ event: 'product list viewed', properties: trackProperties, }, - { - ...commonOutput, - type: 'page', - properties: pageProperties, - }, { type: 'identify', ...commonOutput, @@ -103,7 +89,6 @@ export const data = [ }, metadata: [ { jobId: 1, userId: 'u1' }, - { jobId: 2, userId: 'u1' }, { jobId: 3, userId: 'u1' }, ], batched: true, @@ -142,21 +127,9 @@ export const data = [ { message: { ...commonInput, - type: 'page', - properties: { - title: 'Sample Page', - url: 'https://example.com/?utm_campaign=example_campaign&utm_content=example_content', - path: '/', - hash: '', - search: '?utm_campaign=example_campaign&utm_content=example_content', - width: '1920', - height: '1080', - query: { - utm_campaign: 'example_campaign', - utm_content: 'example_content', - }, - referrer: '', - }, + type: 'track', + event: 'product added', + properties: trackProperties, }, metadata: { jobId: 2, userId: 'u1' }, destination, @@ -210,8 +183,9 @@ export const data = [ }, { ...commonOutput, - type: 'page', - properties: pageProperties, + type: 'track', + event: 'product added', + properties: trackProperties, }, ], }, @@ -264,8 +238,9 @@ export const data = [ { message: { ...commonInput, - type: 'page', - properties: pageProperties, + type: 'track', + event: 'product added', + properties: trackProperties, }, metadata: { jobId: 2, userId: 'u1' }, destination, @@ -330,8 +305,9 @@ export const data = [ }, { ...commonOutput, - type: 'page', - properties: pageProperties, + type: 'track', + event: 'product added', + properties: trackProperties, }, ], }, diff --git a/test/integrations/destinations/rakuten/processor/commonConfig.ts b/test/integrations/destinations/rakuten/processor/commonConfig.ts index e7e2af7fbd..96bc6669c2 100644 --- a/test/integrations/destinations/rakuten/processor/commonConfig.ts +++ b/test/integrations/destinations/rakuten/processor/commonConfig.ts @@ -63,3 +63,27 @@ export const commonProperties = { storeId: '12345', storecat: 'Electronics', }; +export const commonPropertiesWithoutRansiteID = { + orderId: 'SampleOrderId', + landTime: '20240129_1200', + date: '20240129_1300', + altord: 'SampleAlternateOrderId', + currency: 'INR', + creditCardType: 'Visa', + commReason: 'SampleCommReason', + isComm: 'Y', + consumed: '20240129_1400', + coupon: 'SampleCoupon', + custId: 'SampleCustomerId', + custScore: 'A', + custStatus: 'New', + dId: 'SampleDeviceId', + disamt: '50.00', + ordStatus: 'Pending', + segment: 'SampleSegment', + shipcountry: 'USA', + shipped: '20240129_1500', + sitename: 'SampleSiteName', + storeId: '12345', + storecat: 'Electronics', +}; diff --git a/test/integrations/destinations/rakuten/processor/track.ts b/test/integrations/destinations/rakuten/processor/track.ts index 49b26e4658..74d09b8d4c 100644 --- a/test/integrations/destinations/rakuten/processor/track.ts +++ b/test/integrations/destinations/rakuten/processor/track.ts @@ -4,6 +4,7 @@ import { commonProperties, endpoint, singleProductWithAllProperties, + commonPropertiesWithoutRansiteID, } from './commonConfig'; import { transformResultBuilder } from '../../../testUtils'; export const trackSuccess = [ @@ -449,4 +450,129 @@ export const trackSuccess = [ }, }, }, + { + id: 'rakuten-test-track-success-5', + name: 'rakuten', + description: 'Track call no ranSiteId in input', + scenario: 'Business', + successCriteria: + 'Response should contain only properties with tr as empty string (ransiteId) and product payload and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + event: 'product purchased', + sentAt: '2021-01-25T16:12:02.048Z', + userId: 'sajal12', + channel: 'mobile', + rudderId: 'b7b24f86-f7bf-46d8-b2b4-ccafc080239c', + messageId: '1611588776408-ee5a3212-fbf9-4cbb-bbad-3ed0f7c6a2ce', + properties: { + ...commonPropertiesWithoutRansiteID, + products: [ + { ...singleProductWithAllProperties }, + { + sku: 'custom sku 1', + quantity: 5, + amount: 25, + name: 'name_1', + }, + { + sku: 'custom sku 2', + name: 'SampleProduct', + quantity: 1, + amount: 30, + coupon: 'SALE50', + }, + ], + }, + anonymousId: '9c6bd77ea9da3e68', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + output: transformResultBuilder({ + method: 'GET', + endpoint, + headers: commonOutputHeaders, + params: { + mid: 'dummyMarketingId', + xml: 1, + source: 'rudderstack', + amtlist: '2000|2500|3000', + brandlist: 'SampleBrand||', + catidlist: '12345||', + catlist: 'Electronics||', + couponlist: 'SALE20||SALE50', + disamtlist: '10.5||', + distypelist: 'Percentage||', + ismarketplacelist: 'N||', + sequencelist: '123||', + shipbylist: 'Express||', + shipidlist: 'SHIP123||', + qlist: '5|5|1', + marginlist: '0.15||', + markdownlist: '5||', + taxexemptlist: 'N||', + namelist: 'SampleProduct|name_1|SampleProduct', + skulist: 'ABC123|custom sku 1|custom sku 2', + issalelist: 'Y||', + itmstatuslist: 'In Stock||', + isclearancelist: 'Y||', + ord: 'SampleOrderId', + tr: ' ', + land: '20240129_1200', + date: '20240129_1300', + altord: 'SampleAlternateOrderId', + cur: 'INR', + cc: 'Visa', + commreason: 'SampleCommReason', + iscomm: 'Y', + consumed: '20240129_1400', + coupon: 'SampleCoupon', + custid: 'SampleCustomerId', + custscore: 'A', + custstatus: 'New', + did: 'SampleDeviceId', + disamt: '50.00', + ordstatus: 'Pending', + segment: 'SampleSegment', + shipcountry: 'USA', + shipped: '20240129_1500', + sitename: 'SampleSiteName', + storeid: '12345', + storecat: 'Electronics', + }, + userId: '', + }), + statusCode: 200, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/rakuten/processor/transformationFailure.ts b/test/integrations/destinations/rakuten/processor/transformationFailure.ts index e35ab26b69..5485efae03 100644 --- a/test/integrations/destinations/rakuten/processor/transformationFailure.ts +++ b/test/integrations/destinations/rakuten/processor/transformationFailure.ts @@ -202,9 +202,9 @@ export const transformationFailures = [ { id: 'rakuten-test-5', name: 'rakuten', - description: 'No eligible property available for required field tr present', + description: 'No eligible property available for required field land present', scenario: 'Framework', - successCriteria: 'Transformationn Error for required field tr not present', + successCriteria: 'Transformationn Error for required field land not present', feature: 'processor', module: 'destination', version: 'v0', @@ -245,7 +245,7 @@ export const transformationFailures = [ body: [ { error: - 'Missing required value from ["properties.tr","properties.ran_site_id","properties.ranSiteID"]: Workflow: procWorkflow, Step: prepareTrackPayload, ChildStep: undefined, OriginalError: Missing required value from ["properties.tr","properties.ran_site_id","properties.ranSiteID"]', + 'Missing required value from ["properties.land","properties.land_time","properties.landTime"]: Workflow: procWorkflow, Step: prepareTrackPayload, ChildStep: undefined, OriginalError: Missing required value from ["properties.land","properties.land_time","properties.landTime"]', metadata: { destinationId: 'dummyDestId', jobId: '1', diff --git a/test/integrations/destinations/reddit/processor/data.ts b/test/integrations/destinations/reddit/processor/data.ts index 49e0cd2baa..a97ae23d2a 100644 --- a/test/integrations/destinations/reddit/processor/data.ts +++ b/test/integrations/destinations/reddit/processor/data.ts @@ -443,6 +443,8 @@ export const data = [ }, event_metadata: { item_count: 0, + value: 2600, + value_decimal: 26, products: [ { id: '017c6f5d5cf86a4b22432066', @@ -581,6 +583,8 @@ export const data = [ }, event_metadata: { item_count: 5, + value: 24995, + value_decimal: 249.95, products: [ { id: '622c6f5d5cf86a4c77358033', @@ -847,6 +851,8 @@ export const data = [ }, event_metadata: { item_count: 5, + value: 24995, + value_decimal: 249.95, products: [ { id: '622c6f5d5cf86a4c77358033', diff --git a/test/integrations/destinations/reddit/router/data.ts b/test/integrations/destinations/reddit/router/data.ts index 723afff374..b5bed48ae7 100644 --- a/test/integrations/destinations/reddit/router/data.ts +++ b/test/integrations/destinations/reddit/router/data.ts @@ -90,6 +90,8 @@ export const data = [ properties: { list_id: 'list1', category: "What's New", + value: 2600, + value_decimal: 26, products: [ { product_id: '017c6f5d5cf86a4b22432066', @@ -230,6 +232,8 @@ export const data = [ }, event_metadata: { item_count: 0, + value: 2600, + value_decimal: 26, products: [ { id: '017c6f5d5cf86a4b22432066', @@ -259,6 +263,8 @@ export const data = [ }, event_metadata: { item_count: 5, + value: 24995, + value_decimal: 249.95, products: [ { id: '622c6f5d5cf86a4c77358033', @@ -349,6 +355,8 @@ export const data = [ properties: { list_id: 'list1', category: "What's New", + value: 2600, + value_decimal: 26, products: [ { product_id: '017c6f5d5cf86a4b22432066', diff --git a/test/integrations/destinations/sfmc/network.ts b/test/integrations/destinations/sfmc/network.ts index 7564d8c6d5..93854e3691 100644 --- a/test/integrations/destinations/sfmc/network.ts +++ b/test/integrations/destinations/sfmc/network.ts @@ -11,4 +11,54 @@ export const networkCallsData = [ }, }, }, + { + httpReq: { + url: 'https://testHandleHttpRequest401.auth.marketingcloudapis.com/v2/token', + method: 'POST', + }, + httpRes: { + status: 401, + data: { + error: 'invalid_client', + error_description: + 'Invalid client ID. Use the client ID in Marketing Cloud Installed Packages.', + error_uri: 'https://developer.salesforce.com/docs', + }, + }, + }, + { + httpReq: { + url: 'https://testHandleHttpRequest429.auth.marketingcloudapis.com/v2/token', + method: 'POST', + }, + httpRes: { + status: 429, + data: { + message: 'Your requests are temporarily blocked.', + errorcode: 50200, + documentation: + 'https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/error-handling.htm', + }, + }, + }, + { + httpReq: { + url: 'https://testHandleHttpRequest-dns.auth.marketingcloudapis.com/v2/token', + method: 'POST', + }, + httpRes: { + data: {}, + status: 400, + }, + }, + { + httpReq: { + url: 'https://testHandleHttpRequest-null.auth.marketingcloudapis.com/v2/token', + method: 'POST', + }, + httpRes: { + data: null, + status: 500, + }, + }, ]; diff --git a/test/integrations/destinations/sfmc/processor/data.ts b/test/integrations/destinations/sfmc/processor/data.ts index b2839908ad..883032d223 100644 --- a/test/integrations/destinations/sfmc/processor/data.ts +++ b/test/integrations/destinations/sfmc/processor/data.ts @@ -1894,4 +1894,326 @@ export const data = [ }, }, }, + { + name: 'sfmc', + description: 'Tests 401 un authenticated code from sfmc', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + event: 'message event', + type: 'track', + userId: '12345', + properties: { + id: 'id101', + contactId: 'cid101', + email: 'testemail@gmail.com', + accountNumber: '99110099', + patronName: 'SP', + }, + }, + destination: { + ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq', + Name: 'SFMC', + DestinationDefinition: { + ID: '1pYpYSeQd8OeN6xPdw6VGDzqUd1', + Name: 'SFMC', + DisplayName: 'Salesforce Marketing Cloud', + Config: { + destConfig: [], + excludeKeys: [], + includeKeys: [], + saveDestinationResponse: false, + supportedSourceTypes: [], + transformAt: 'processor', + }, + ResponseRules: {}, + }, + Config: { + clientId: 'testHandleHttpRequest401', + clientSecret: 'testHandleHttpRequest401', + createOrUpdateContacts: false, + eventDelivery: true, + eventDeliveryTS: 1615371070621, + eventToExternalKey: [ + { + from: 'Event Name', + to: 'C500FD37-155C-49BD-A21B-AFCEF3D1A9CB', + }, + { + from: 'Watch', + to: 'C500FD37-155C-49BD-A21B-AFCEF3D1A9CB', + }, + ], + eventToPrimaryKey: [ + { + from: 'userId', + to: 'User Key', + }, + { + from: 'watch', + to: 'Guest Key, Contact Key', + }, + ], + eventToUUID: [ + { + event: 'Event Name', + uuid: true, + }, + ], + eventToDefinitionMapping: [ + { + from: 'message event', + to: 'test-event-definition', + }, + ], + externalKey: 'f3ffa19b-e0b3-4967-829f-549b781080e6', + subDomain: 'testHandleHttpRequest401', + }, + Enabled: true, + Transformations: [], + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '{"message":"Could not retrieve access token","destinationResponse":{"error":"invalid_client","error_description":"Invalid client ID. Use the client ID in Marketing Cloud Installed Packages.","error_uri":"https://developer.salesforce.com/docs"}}', + statTags: { + destType: 'SFMC', + errorCategory: 'network', + errorType: 'aborted', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 401, + }, + ], + }, + }, + }, + { + name: 'sfmc', + description: 'Tests 429 status code from sfmc', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + event: 'message event', + type: 'track', + userId: '12345', + properties: { + id: 'id101', + contactId: 'cid101', + email: 'testemail@gmail.com', + accountNumber: '99110099', + patronName: 'SP', + }, + }, + destination: { + ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq', + Name: 'SFMC', + DestinationDefinition: { + ID: '1pYpYSeQd8OeN6xPdw6VGDzqUd1', + Name: 'SFMC', + DisplayName: 'Salesforce Marketing Cloud', + Config: { + destConfig: [], + excludeKeys: [], + includeKeys: [], + saveDestinationResponse: false, + supportedSourceTypes: [], + transformAt: 'processor', + }, + ResponseRules: {}, + }, + Config: { + clientId: 'testHandleHttpRequest429', + clientSecret: 'testHandleHttpRequest429', + subDomain: 'testHandleHttpRequest429', + }, + Enabled: true, + Transformations: [], + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + '{"message":"Could not retrieve access token","destinationResponse":{"message":"Your requests are temporarily blocked.","errorcode":50200,"documentation":"https://developer.salesforce.com/docs/atlas.en-us.mc-apis.meta/mc-apis/error-handling.htm"}}', + statTags: { + destType: 'SFMC', + errorCategory: 'network', + errorType: 'throttled', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 429, + }, + ], + }, + }, + }, + { + name: 'sfmc', + description: 'Tests DNS lookup failure for sfmc', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + event: 'message event', + type: 'track', + userId: '12345', + properties: { + id: 'id101', + contactId: 'cid101', + email: 'testemail@gmail.com', + accountNumber: '99110099', + patronName: 'SP', + }, + }, + destination: { + ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq', + Name: 'SFMC', + DestinationDefinition: { + ID: '1pYpYSeQd8OeN6xPdw6VGDzqUd1', + Name: 'SFMC', + DisplayName: 'Salesforce Marketing Cloud', + Config: { + destConfig: [], + excludeKeys: [], + includeKeys: [], + saveDestinationResponse: false, + supportedSourceTypes: [], + transformAt: 'processor', + }, + ResponseRules: {}, + }, + Config: { + clientId: 'testHandleHttpRequest-dns', + clientSecret: 'testHandleHttpRequest-dns', + subDomain: 'testHandleHttpRequest-dns', + }, + Enabled: true, + Transformations: [], + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: '{"message":"Could not retrieve access token","destinationResponse":{}}', + statTags: { + destType: 'SFMC', + errorCategory: 'network', + errorType: 'aborted', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 400, + }, + ], + }, + }, + }, + { + name: 'sfmc', + description: 'Test 500 status failure for sfmc', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + event: 'message event', + type: 'track', + userId: '12345', + properties: { + id: 'id101', + contactId: 'cid101', + email: 'testemail@gmail.com', + accountNumber: '99110099', + patronName: 'SP', + }, + }, + destination: { + ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq', + Name: 'SFMC', + DestinationDefinition: { + ID: '1pYpYSeQd8OeN6xPdw6VGDzqUd1', + Name: 'SFMC', + DisplayName: 'Salesforce Marketing Cloud', + Config: { + destConfig: [], + excludeKeys: [], + includeKeys: [], + saveDestinationResponse: false, + supportedSourceTypes: [], + transformAt: 'processor', + }, + ResponseRules: {}, + }, + Config: { + clientId: 'testHandleHttpRequest-null', + clientSecret: 'testHandleHttpRequest-null', + subDomain: 'testHandleHttpRequest-null', + }, + Enabled: true, + Transformations: [], + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: 'Could not retrieve access token', + statTags: { + destType: 'SFMC', + errorCategory: 'network', + errorType: 'retryable', + feature: 'processor', + implementation: 'native', + module: 'destination', + }, + statusCode: 500, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/snapchat_conversion/processor/data.ts b/test/integrations/destinations/snapchat_conversion/processor/data.ts index b0d14208cc..7de7ed9b8d 100644 --- a/test/integrations/destinations/snapchat_conversion/processor/data.ts +++ b/test/integrations/destinations/snapchat_conversion/processor/data.ts @@ -4600,6 +4600,140 @@ export const data = [ }, }, }, + { + name: 'snapchat_conversion', + description: 'test event mapping from destination config', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + messageId: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be', + originalTimestamp: '2022-04-22T10:57:58Z', + anonymousId: 'ea5cfab2-3961-4d8a-8187-3d1858c99090', + context: { + traits: { + email: 'test@email.com', + phone: '+91 2111111 ', + }, + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + device: { + advertisingId: 'T0T0T072-5e28-45a1-9eda-ce22a3e36d1a', + id: '3f034872-5e28-45a1-9eda-ce22a3e36d1a', + manufacturer: 'Google', + name: 'generic_x86_arm', + type: 'ios', + attTrackingStatus: 3, + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + locale: 'en-US', + os: { + name: 'iOS', + version: '14.4.1', + }, + 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', + }, + type: 'track', + event: 'Custom Event', + properties: { + query: 't-shirts', + event_conversion_type: 'web', + }, + integrations: { + All: true, + }, + sentAt: '2022-04-22T10:57:58Z', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: false, + }, + }, + Config: { + pixelId: 'dummyPixelId', + apiKey: 'dummyApiKey', + rudderEventsToSnapEvents: [ + { + from: 'Custom Event', + to: 'level_complete', + }, + ], + }, + }, + metadata: { + jobId: 47, + destinationId: 'd2', + workspaceId: 'w2', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + jobId: 47, + destinationId: 'd2', + workspaceId: 'w2', + }, + output: { + version: '1', + type: 'REST', + userId: '', + method: 'POST', + endpoint: 'https://tr.snapchat.com/v2/conversion', + headers: { + Authorization: 'Bearer dummyApiKey', + 'Content-Type': 'application/json', + }, + params: {}, + body: { + JSON: { + event_type: 'LEVEL_COMPLETE', + hashed_email: '73062d872926c2a556f17b36f50e328ddf9bff9d403939bd14b6c3b7f5a33fc2', + hashed_phone_number: + 'bc77d64d7045fe44795ed926df37231a0cfb6ec6b74588c512790e9f143cc492', + hashed_mobile_ad_id: + 'f9779d734aaee50f16ee0011260bae7048f1d9a128c62b6a661077875701edd2', + hashed_idfv: '54bd0b26a3d39dad90f5149db49b9fd9ba885f8e35d1d94cae69273f5e657b9f', + user_agent: + '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', + timestamp: '1650625078', + event_conversion_type: 'OFFLINE', + pixel_id: 'dummyPixelId', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + statusCode: 200, + }, + ], + }, + }, + }, ].map((tc) => ({ ...tc, mockFns: (_) => { diff --git a/test/integrations/destinations/twitter_ads/router/data.ts b/test/integrations/destinations/twitter_ads/router/data.ts new file mode 100644 index 0000000000..ce9aea6595 --- /dev/null +++ b/test/integrations/destinations/twitter_ads/router/data.ts @@ -0,0 +1,204 @@ +const authHeaderConstant = + 'OAuth oauth_consumer_key="qwe", oauth_nonce="V1kMh028kZLLhfeYozuL0B45Pcx6LvuW", oauth_signature="Di4cuoGv4PnCMMEeqfWTcqhvdwc%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1685603652", oauth_token="dummyAccessToken", oauth_version="1.0"'; + +export const data = [ + { + name: 'twitter_ads', + description: 'tests router flow', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'track', + event: 'ABC Searched', + channel: 'web', + context: { + source: 'test', + userAgent: 'chrome', + traits: { + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + email: 'abc@gmail.com', + phone: '+1234589947', + ge: 'male', + db: '19950715', + lastname: 'Rudderlabs', + firstName: 'Test', + address: { + city: 'Kolkata', + state: 'WB', + zip: '700114', + country: 'IN', + }, + }, + device: { + advertisingId: 'abc123', + }, + library: { + name: 'rudder-sdk-ruby-sync', + version: '1.0.6', + }, + }, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2020-08-14T05:30:30.118Z', + properties: { + conversionTime: '2023-06-01T06:03:08.739Z', + tax: 2, + total: 27.5, + coupon: 'hasbros', + revenue: 48, + price: 25, + quantity: 2, + currency: 'USD', + priceCurrency: 'USD', + conversionId: '213123', + numberItems: '2323', + phone: '+919927455678', + twclid: '543', + shipping: 3, + subtotal: 22.5, + affiliation: 'Google Store', + checkout_id: 'fksdjfsdjfisjf9sdfjsd9f', + email: 'abc@ax.com', + contents: [ + { + price: '123.3345', + quantity: '12', + id: '12', + }, + { + price: 200, + quantity: 11, + id: '4', + }, + ], + }, + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { + All: true, + }, + }, + metadata: { + secret: { + consumerKey: 'qwe', + consumerSecret: 'fdghv', + accessToken: 'dummyAccessToken', + accessTokenSecret: 'testAccessTokenSecret', + }, + }, + destination: { + Config: { + pixelId: 'dummyPixelId', + rudderAccountId: '2EOknn1JNH7WK1MfNku4fGYKkRK', + twitterAdsEventNames: [ + { + rudderEventName: 'ABC Searched', + twitterEventId: 'tw-234234324234', + }, + { + rudderEventName: 'Home Page Viewed', + twitterEventId: 'tw-odt2o-odt2q', + }, + ], + }, + }, + }, + ], + destType: 'twitter_ads', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + FORM: {}, + JSON: { + conversions: [ + { + contents: [ + { content_id: '12', content_price: 123.3345, num_items: 12 }, + { content_id: '4', content_price: 200, num_items: 11 }, + ], + conversion_id: '213123', + conversion_time: '2023-06-01T06:03:08.739Z', + event_id: 'tw-234234324234', + identifiers: [ + { + hashed_email: + '4c3c8a8cba2f3bb1e9e617301f85d1f68e816a01c7b716f482f2ab9adb8181fb', + }, + { + hashed_phone_number: + 'b308962b96b40cce7981493a372db9478edae79f83c2d8ca6cd15a39566f8c56', + }, + { twclid: '543' }, + ], + number_items: 2, + price_currency: 'USD', + value: '25', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://ads-api.twitter.com/12/measurement/conversions/dummyPixelId', + files: {}, + headers: { + Authorization: + 'OAuth oauth_consumer_key="qwe", oauth_nonce="V1kMh028kZLLhfeYozuL0B45Pcx6LvuW", oauth_signature="Di4cuoGv4PnCMMEeqfWTcqhvdwc%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1685603652", oauth_token="dummyAccessToken", oauth_version="1.0"', + 'Content-Type': 'application/json', + }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: { + Config: { + pixelId: 'dummyPixelId', + rudderAccountId: '2EOknn1JNH7WK1MfNku4fGYKkRK', + twitterAdsEventNames: [ + { rudderEventName: 'ABC Searched', twitterEventId: 'tw-234234324234' }, + { rudderEventName: 'Home Page Viewed', twitterEventId: 'tw-odt2o-odt2q' }, + ], + }, + }, + metadata: [ + { + secret: { + accessToken: 'dummyAccessToken', + accessTokenSecret: 'testAccessTokenSecret', + consumerKey: 'qwe', + consumerSecret: 'fdghv', + }, + }, + ], + statusCode: 200, + }, + ], + }, + }, + }, + }, +].map((tc) => ({ + ...tc, + mockFns: (_) => { + jest.mock('../../../../../src/v0/destinations/twitter_ads/util', () => ({ + getAuthHeaderForRequest: (_a, _b) => { + return { Authorization: authHeaderConstant }; + }, + })); + }, +})); diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts index 13a76702f9..0a2727f4d0 100644 --- a/test/integrations/testUtils.ts +++ b/test/integrations/testUtils.ts @@ -292,6 +292,7 @@ export const generatePageOrScreenPayload: any = (parametersOverride: any, eventT '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', }), event: parametersOverride.event, + name: parametersOverride.name, anonymousId: parametersOverride.anonymousId || 'default-anonymousId', properties: parametersOverride.properties, type: eventType || 'page', @@ -484,7 +485,7 @@ export const generateProxyV1Payload = ( workspaceId: 'default-workspaceId', sourceId: 'default-sourceId', secret: { - accessToken: 'default-accessToken', + accessToken: payloadParameters.accessToken || 'default-accessToken', }, dontBatch: false, },