diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index fe308ce8b9..cfcb1fc0d8 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -26,6 +26,8 @@ N/A
N/A
+@coderabbitai review
+
### Developer checklist
diff --git a/.github/workflows/component-test-report.yml b/.github/workflows/component-test-report.yml
index 9904004417..e41ca0a723 100644
--- a/.github/workflows/component-test-report.yml
+++ b/.github/workflows/component-test-report.yml
@@ -28,7 +28,7 @@ jobs:
fetch-depth: 1
- name: Setup Node
- uses: actions/setup-node@v4.0.1
+ uses: actions/setup-node@v4.0.2
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -40,13 +40,11 @@ jobs:
run: |
npm run test:ts -- component
-
- name: Uplaod Report to S3
run: |
aws s3 cp ./test_reports/ s3://test-integrations-dev/integrations-test-reports/rudder-transformer/${{ github.event.number }}/ --recursive
-
- - name: Comment on PR with S3 Object URL
+ - name: Add Test Report Link as Comment on PR
uses: actions/github-script@v7
with:
github-token: ${{ secrets.PAT }}
@@ -55,10 +53,23 @@ jobs:
// Get the pull request number
const prNumber = context.payload.pull_request.number;
const commentBody = `Test report for this run is available at: https://test-integrations-dev.s3.amazonaws.com/integrations-test-reports/rudder-transformer/${prNumber}/test-report.html`;
-
+
+ // find all the comments of the PR
+ const issueComments = await github.paginate(github.rest.issues.listComments, {
+ owner,
+ repo,
+ issue_number: prNumber,
+ });
+
+ for (const comment of issueComments) {
+ if (comment.body === commentBody) {
+ console.log('Comment already exists');
+ return;
+ }
+ }
// Comment on the pull request
- github.rest.issues.createComment({
+ await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
diff --git a/.github/workflows/create-hotfix-branch.yml b/.github/workflows/create-hotfix-branch.yml
index a164c25bee..994283a9f4 100644
--- a/.github/workflows/create-hotfix-branch.yml
+++ b/.github/workflows/create-hotfix-branch.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
# Only allow these users to create new hotfix branch from 'main'
- if: github.ref == 'refs/heads/main' && (github.actor == 'ItsSudip' || github.actor == 'krishna2020' || github.actor == 'koladilip' || github.actor == 'saikumarrs' || github.actor == 'sandeepdsvs' || github.actor == 'shrouti1507' || github.actor == 'anantjain45823' || github.actor == 'chandumlg' || github.actor == 'mihir-4116' || github.actor == 'ujjwal-ab') && (github.triggering_actor == 'ItsSudip' || github.triggering_actor == 'krishna2020' || github.triggering_actor == 'saikumarrs' || github.triggering_actor == 'sandeepdsvs' || github.triggering_actor == 'koladilip' || github.triggering_actor == 'shrouti1507' || github.triggering_actor == 'anantjain45823' || github.triggering_actor == 'chandumlg' || github.triggering_actor == 'mihir-4116' || github.triggering_actor == 'ujjwal-ab')
+ if: github.ref == 'refs/heads/main' && (github.actor == 'ItsSudip' || github.actor == 'krishna2020' || github.actor == 'koladilip' || github.actor == 'saikumarrs' || github.actor == 'sandeepdsvs' || github.actor == 'shrouti1507' || github.actor == 'anantjain45823' || github.actor == 'chandumlg' || github.actor == 'mihir-4116' || github.actor == 'utsabc') && (github.triggering_actor == 'ItsSudip' || github.triggering_actor == 'krishna2020' || github.triggering_actor == 'saikumarrs' || github.triggering_actor == 'sandeepdsvs' || github.triggering_actor == 'koladilip' || github.triggering_actor == 'shrouti1507' || github.triggering_actor == 'anantjain45823' || github.triggering_actor == 'chandumlg' || github.triggering_actor == 'mihir-4116' || github.triggering_actor == 'sanpj2292' || github.triggering_actor == 'utsabc')
steps:
- name: Create Branch
uses: peterjgrainger/action-create-branch@v2.4.0
diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml
index 33b0396705..95431fef8e 100644
--- a/.github/workflows/draft-new-release.yml
+++ b/.github/workflows/draft-new-release.yml
@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-latest
# Only allow release stakeholders to initiate releases
- if: (github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/hotfix/')) && (github.actor == 'ItsSudip' || github.actor == 'krishna2020' || github.actor == 'saikumarrs' || github.actor == 'sandeepdsvs' || github.actor == 'koladilip' || github.actor == 'shrouti1507' || github.actor == 'anantjain45823' || github.actor == 'chandumlg' || github.actor == 'mihir-4116' || github.actor == 'yashasvibajpai' || github.actor == 'sanpj2292' || github.actor == 'ujjwal-ab') && (github.triggering_actor == 'ItsSudip' || github.triggering_actor == 'krishna2020' || github.triggering_actor == 'koladilip' || github.triggering_actor == 'saikumarrs' || github.triggering_actor == 'sandeepdsvs' || github.triggering_actor == 'shrouti1507' || github.triggering_actor == 'anantjain45823' || github.triggering_actor == 'chandumlg' || github.triggering_actor == 'mihir-4116' || github.triggering_actor == 'yashasvibajpai' || github.triggering_actor == 'sanpj2292' || github.triggering_actor == 'ujjwal-ab')
+ if: (github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/hotfix/')) && (github.actor == 'ItsSudip' || github.actor == 'krishna2020' || github.actor == 'saikumarrs' || github.actor == 'sandeepdsvs' || github.actor == 'koladilip' || github.actor == 'shrouti1507' || github.actor == 'anantjain45823' || github.actor == 'chandumlg' || github.actor == 'mihir-4116' || github.actor == 'yashasvibajpai' || github.actor == 'sanpj2292' || github.actor == 'utsabc') && (github.triggering_actor == 'ItsSudip' || github.triggering_actor == 'krishna2020' || github.triggering_actor == 'koladilip' || github.triggering_actor == 'saikumarrs' || github.triggering_actor == 'sandeepdsvs' || github.triggering_actor == 'shrouti1507' || github.triggering_actor == 'anantjain45823' || github.triggering_actor == 'chandumlg' || github.triggering_actor == 'mihir-4116' || github.triggering_actor == 'yashasvibajpai' || github.triggering_actor == 'sanpj2292' || github.triggering_actor == 'utsabc')
steps:
- name: Checkout
uses: actions/checkout@v4.1.1
@@ -16,7 +16,7 @@ jobs:
fetch-depth: 0
- name: Setup Node
- uses: actions/setup-node@v4.0.1
+ uses: actions/setup-node@v4.0.2
with:
node-version-file: '.nvmrc'
cache: 'npm'
diff --git a/.github/workflows/dt-test-and-report-code-coverage.yml b/.github/workflows/dt-test-and-report-code-coverage.yml
index 4096227400..51a9f8c9ee 100644
--- a/.github/workflows/dt-test-and-report-code-coverage.yml
+++ b/.github/workflows/dt-test-and-report-code-coverage.yml
@@ -20,7 +20,7 @@ jobs:
fetch-depth: 1
- name: Setup Node
- uses: actions/setup-node@v4.0.1
+ uses: actions/setup-node@v4.0.2
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -40,12 +40,14 @@ jobs:
npm run lint:fix
- name: Upload Coverage Reports to Codecov
- uses: codecov/codecov-action@v3.1.4
+ uses: codecov/codecov-action@v4.0.1
with:
directory: ./reports/coverage
- name: Upload TS Coverage Reports to Codecov
- uses: codecov/codecov-action@v3.1.4
+ uses: codecov/codecov-action@v4.0.1
+ env:
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
directory: ./reports/ts-coverage
diff --git a/.github/workflows/publish-new-release.yml b/.github/workflows/publish-new-release.yml
index 9d1558d826..233e99577d 100644
--- a/.github/workflows/publish-new-release.yml
+++ b/.github/workflows/publish-new-release.yml
@@ -30,7 +30,7 @@ jobs:
fetch-depth: 0
- name: Setup Node
- uses: actions/setup-node@v4.0.1
+ uses: actions/setup-node@v4.0.2
with:
node-version-file: '.nvmrc'
cache: 'npm'
@@ -89,7 +89,7 @@ jobs:
- name: Notify Slack Channel
id: slack
- uses: slackapi/slack-github-action@v1.24.0
+ uses: slackapi/slack-github-action@v1.25.0
continue-on-error: true
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
diff --git a/.github/workflows/ut-tests.yml b/.github/workflows/ut-tests.yml
index 30c29ceaee..0a2ef8a390 100644
--- a/.github/workflows/ut-tests.yml
+++ b/.github/workflows/ut-tests.yml
@@ -26,7 +26,7 @@ jobs:
fetch-depth: 1
- name: Setup Node
- uses: actions/setup-node@v4.0.1
+ uses: actions/setup-node@v4.0.2
with:
node-version-file: '.nvmrc'
cache: 'npm'
diff --git a/.gitignore b/.gitignore
index f96c3ac807..956605f139 100644
--- a/.gitignore
+++ b/.gitignore
@@ -132,7 +132,7 @@ dist
# Others
**/.DS_Store
-
+.dccache
.idea
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08032253ac..d67c45a147 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,28 @@
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.56.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.55.0...v1.56.0) (2024-02-19)
+
+
+### Features
+
+* **hs:** chunking data based on batch limit ([#2907](https://github.com/rudderlabs/rudder-transformer/issues/2907)) ([a60694c](https://github.com/rudderlabs/rudder-transformer/commit/a60694cef1da31d27a5cf90264548cad793f556f))
+* onboard bluecore integration ([#3061](https://github.com/rudderlabs/rudder-transformer/issues/3061)) ([aef5f8e](https://github.com/rudderlabs/rudder-transformer/commit/aef5f8e5f267262e0f9e10229f14f2bcc8ad29e2))
+* tiktok_offline_events added support for all Standard events ([#3094](https://github.com/rudderlabs/rudder-transformer/issues/3094)) ([b5cdccb](https://github.com/rudderlabs/rudder-transformer/commit/b5cdccb75fe68150816140174087fddad677db10))
+
+
+### Bug Fixes
+
+* add support of placing properties at root in af ([#3082](https://github.com/rudderlabs/rudder-transformer/issues/3082)) ([0f01524](https://github.com/rudderlabs/rudder-transformer/commit/0f01524b6f4f2f82efc21f88f8c97cb6fdaf91ea))
+* amplitude batch output metadata ([#3077](https://github.com/rudderlabs/rudder-transformer/issues/3077)) ([69c8348](https://github.com/rudderlabs/rudder-transformer/commit/69c83489c85486c9b2aed4a1292bd9f0aae9ca44))
+* amplitude: Error handling for missing event type ([#3079](https://github.com/rudderlabs/rudder-transformer/issues/3079)) ([f7ec0a1](https://github.com/rudderlabs/rudder-transformer/commit/f7ec0a1244a7b97e6b40de5ed9881c63300866dc))
+* custify user-regulation logic ([#3076](https://github.com/rudderlabs/rudder-transformer/issues/3076)) ([9683161](https://github.com/rudderlabs/rudder-transformer/commit/9683161612c7e3b9c2be95a2728f68ec7dcf69f4))
+* error handling for auth0 source ([#3038](https://github.com/rudderlabs/rudder-transformer/issues/3038)) ([2a21274](https://github.com/rudderlabs/rudder-transformer/commit/2a21274333350c615991f7b56b81b766502d5bf4))
+* **ga4:** failures not considered with 200 status in events tab ([#3089](https://github.com/rudderlabs/rudder-transformer/issues/3089)) ([6a364fb](https://github.com/rudderlabs/rudder-transformer/commit/6a364fba34c46b15c0fe4b06ecfa6f4b81b6f436))
+* gaoc batching order ([#3064](https://github.com/rudderlabs/rudder-transformer/issues/3064)) ([a98cabd](https://github.com/rudderlabs/rudder-transformer/commit/a98cabdfe7781ada12baf742df4a3a439fc5fecd))
+* resolve bugsnag issue caused due to undefined properties ([#3086](https://github.com/rudderlabs/rudder-transformer/issues/3086)) ([d522b35](https://github.com/rudderlabs/rudder-transformer/commit/d522b35c908a9f262ba3ba27dda0ea5d9ac5bc6b))
+* tiktok ads v2 error handling ([#3084](https://github.com/rudderlabs/rudder-transformer/issues/3084)) ([b6edff4](https://github.com/rudderlabs/rudder-transformer/commit/b6edff46fa0e0e210e82206fea46a064e3fbe00f))
+
## [1.55.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.54.4...v1.55.0) (2024-02-05)
diff --git a/README.md b/README.md
index 2b3908dcb5..4ff1cd4b34 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,8 @@
+
+⚠️ Docker image for rudder-transformer has been moved to new org rudderstack/rudder-transformer
+
+
+
[![codecov](https://codecov.io/gh/rudderlabs/rudder-transformer/branch/develop/graph/badge.svg?token=G24OON85SB)](https://codecov.io/gh/rudderlabs/rudder-transformer)
# RudderStack Transformer
diff --git a/package-lock.json b/package-lock.json
index b711cd7f72..2a51a2b916 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "rudder-transformer",
- "version": "1.55.0",
+ "version": "1.56.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rudder-transformer",
- "version": "1.55.0",
+ "version": "1.56.0",
"license": "ISC",
"dependencies": {
"@amplitude/ua-parser-js": "0.7.24",
@@ -20,7 +20,7 @@
"@ndhoule/extend": "^2.0.0",
"@pyroscope/nodejs": "^0.2.6",
"@rudderstack/integrations-lib": "^0.2.2",
- "@rudderstack/workflow-engine": "^0.6.9",
+ "@rudderstack/workflow-engine": "^0.7.2",
"ajv": "^8.12.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^2.1.1",
@@ -4536,17 +4536,17 @@
}
},
"node_modules/@rudderstack/json-template-engine": {
- "version": "0.8.2",
- "resolved": "https://registry.npmjs.org/@rudderstack/json-template-engine/-/json-template-engine-0.8.2.tgz",
- "integrity": "sha512-9oMBnqgNuwiXd7MUlNOAchCnJXQAy6w6XGmDqDM6iXdYDkvqYFiq7sbg5j4SdtpTTST293hahREr5PXfFVzVKg=="
+ "version": "0.8.5",
+ "resolved": "https://registry.npmjs.org/@rudderstack/json-template-engine/-/json-template-engine-0.8.5.tgz",
+ "integrity": "sha512-+iH40g+ZA2ANgwjOITdEdZJLZV+ljR28Akn/dRoDia591tMu7PptyvDaAvl+m1DijWXddpLQ8SX9xaEcIdmqlw=="
},
"node_modules/@rudderstack/workflow-engine": {
- "version": "0.6.10",
- "resolved": "https://registry.npmjs.org/@rudderstack/workflow-engine/-/workflow-engine-0.6.10.tgz",
- "integrity": "sha512-3GRdnbB0BuSPWiKf4JsSpG7QuGffAFWkT5T0JLR7Jxps25gt+PgtjQiAlwrRhO5A0WeTJMIKTI7ctz6dGmJosg==",
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@rudderstack/workflow-engine/-/workflow-engine-0.7.2.tgz",
+ "integrity": "sha512-aXQvoXMekvXxxDG6Yc5P5l3PJIwqVA+EmJ2w4SnQ94BUHhbsybPjgGvyzD17MUTAdWEOtqS38SuzLflBs/5T4g==",
"dependencies": {
"@aws-crypto/sha256-js": "^5.0.0",
- "@rudderstack/json-template-engine": "^0.8.1",
+ "@rudderstack/json-template-engine": "^0.8.4",
"jsonata": "^2.0.3",
"lodash": "^4.17.21",
"object-sizeof": "^2.6.3",
diff --git a/package.json b/package.json
index 8002398f5c..bd7c3619fb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "rudder-transformer",
- "version": "1.55.0",
+ "version": "1.56.0",
"description": "",
"homepage": "https://github.com/rudderlabs/rudder-transformer#readme",
"bugs": {
@@ -65,7 +65,7 @@
"@ndhoule/extend": "^2.0.0",
"@pyroscope/nodejs": "^0.2.6",
"@rudderstack/integrations-lib": "^0.2.2",
- "@rudderstack/workflow-engine": "^0.6.9",
+ "@rudderstack/workflow-engine": "^0.7.2",
"ajv": "^8.12.0",
"ajv-draft-04": "^1.0.0",
"ajv-formats": "^2.1.1",
diff --git a/src/adapters/network.js b/src/adapters/network.js
index b0bd14374e..d759412b7a 100644
--- a/src/adapters/network.js
+++ b/src/adapters/network.js
@@ -49,11 +49,15 @@ const fireHTTPStats = (clientResponse, startTime, statTags) => {
const destType = statTags.destType ? statTags.destType : '';
const feature = statTags.feature ? statTags.feature : '';
const endpointPath = statTags.endpointPath ? statTags.endpointPath : '';
+ const requestMethod = statTags.requestMethod ? statTags.requestMethod : '';
+ const module = statTags.module ? statTags.module : '';
const statusCode = clientResponse.success ? clientResponse.response.status : '';
stats.timing('outgoing_request_latency', startTime, {
feature,
destType,
endpointPath,
+ requestMethod,
+ module
});
stats.counter('outgoing_request_count', 1, {
feature,
@@ -61,6 +65,8 @@ const fireHTTPStats = (clientResponse, startTime, statTags) => {
endpointPath,
success: clientResponse.success,
statusCode,
+ requestMethod,
+ module
});
};
diff --git a/src/cdk/v2/destinations/bluecore/config.js b/src/cdk/v2/destinations/bluecore/config.js
new file mode 100644
index 0000000000..9b9cde9c66
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/config.js
@@ -0,0 +1,56 @@
+const { getMappingConfig } = require('../../../../v0/util');
+
+const BASE_URL = 'https://api.bluecore.com/api/track/mobile/v1';
+
+const CONFIG_CATEGORIES = {
+ IDENTIFY: {
+ name: 'bluecoreIdentifyConfig',
+ type: 'identify',
+ },
+ TRACK: {
+ name: 'bluecoreTrackConfig',
+ type: 'track',
+ },
+ COMMON: {
+ name: 'bluecoreCommonConfig',
+ type: 'common',
+ },
+};
+
+const EVENT_NAME_MAPPING = [
+ {
+ src: ['product viewed'],
+ dest: 'viewed_product',
+ },
+ {
+ src: ['products searched'],
+ dest: 'search',
+ },
+ {
+ src: ['product added'],
+ dest: 'add_to_cart',
+ },
+ {
+ src: ['product removed'],
+ dest: 'remove_from_cart',
+ },
+ {
+ src: ['product added to wishlist'],
+ dest: 'wishlist',
+ },
+ {
+ src: ['order completed'],
+ dest: 'purchase',
+ },
+];
+
+const BLUECORE_EXCLUSION_FIELDS = ['query', 'order_id', 'total'];
+
+const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);
+module.exports = {
+ CONFIG_CATEGORIES,
+ MAPPING_CONFIG,
+ EVENT_NAME_MAPPING,
+ BASE_URL,
+ BLUECORE_EXCLUSION_FIELDS,
+};
diff --git a/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json b/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json
new file mode 100644
index 0000000000..be74c7c4b3
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/data/bluecoreCommonConfig.json
@@ -0,0 +1,52 @@
+[
+ {
+ "destKey": "properties.customer.name",
+ "sourceKeys": "name",
+ "required": false,
+ "sourceFromGenericMap": true
+ },
+ {
+ "destKey": "properties.customer.first_name",
+ "sourceKeys": "firstName",
+ "required": false,
+ "sourceFromGenericMap": true
+ },
+ {
+ "destKey": "properties.customer.last_name",
+ "sourceKeys": "lastName",
+ "required": false,
+ "sourceFromGenericMap": true
+ },
+ {
+ "destKey": "properties.customer.age",
+ "sourceKeys": ["context.traits.age", "traits.age"],
+ "required": false
+ },
+ {
+ "destKey": "properties.customer.sex",
+ "sourceKeys": ["traits.gender", "context.traits.gender", "traits.sex", "context.traits.sex"],
+ "required": false
+ },
+ {
+ "destKey": "properties.customer.address",
+ "sourceKeys": "address",
+ "required": false,
+ "sourceFromGenericMap": true
+ },
+ {
+ "destKey": "properties.customer.email",
+ "sourceKeys": "email",
+ "required": false,
+ "sourceFromGenericMap": true
+ },
+ {
+ "destKey": "properties.client",
+ "sourceKeys": "context.app.version",
+ "required": false
+ },
+ {
+ "destKey": "properties.device",
+ "sourceKeys": "context.device.model",
+ "required": false
+ }
+]
diff --git a/src/cdk/v2/destinations/bluecore/data/bluecoreIdentifyConfig.json b/src/cdk/v2/destinations/bluecore/data/bluecoreIdentifyConfig.json
new file mode 100644
index 0000000000..5c3686f0ab
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/data/bluecoreIdentifyConfig.json
@@ -0,0 +1,7 @@
+[
+ {
+ "destKey": "event",
+ "sourceKeys": ["traits.action", "context.traits.action"],
+ "required": false
+ }
+]
diff --git a/src/cdk/v2/destinations/bluecore/data/bluecoreTrackConfig.json b/src/cdk/v2/destinations/bluecore/data/bluecoreTrackConfig.json
new file mode 100644
index 0000000000..8f6d59ec54
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/data/bluecoreTrackConfig.json
@@ -0,0 +1,22 @@
+[
+ {
+ "destKey": "properties.search_term",
+ "sourceKeys": "properties.query",
+ "required": false
+ },
+ {
+ "destKey": "properties.order_id",
+ "sourceKeys": "properties.order_id",
+ "required": false
+ },
+ {
+ "destKey": "properties.total",
+ "sourceKeys": "properties.total",
+ "required": false
+ },
+ {
+ "destKey": "properties.products",
+ "sourceKeys": ["properties.products"],
+ "required": false
+ }
+]
diff --git a/src/cdk/v2/destinations/bluecore/procWorkflow.yaml b/src/cdk/v2/destinations/bluecore/procWorkflow.yaml
new file mode 100644
index 0000000000..378659fa2a
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/procWorkflow.yaml
@@ -0,0 +1,69 @@
+bindings:
+ - name: EventType
+ path: ../../../../constants
+ - path: ../../bindings/jsontemplate
+ - name: defaultRequestConfig
+ path: ../../../../v0/util
+ - name: removeUndefinedNullValuesAndEmptyObjectArray
+ path: ../../../../v0/util
+ - name: removeUndefinedAndNullValues
+ path: ../../../../v0/util
+ - path: ./utils
+ - path: lodash
+ name: cloneDeep
+
+steps:
+ - name: messageType
+ template: |
+ .message.type.toLowerCase();
+ - name: validateInput
+ template: |
+ let messageType = $.outputs.messageType;
+ $.assert(messageType, "message Type is not present. Aborting");
+ $.assert(messageType in {{$.EventType.([.TRACK, .IDENTIFY])}}, "message type " + messageType + " is not supported");
+ $.assertConfig(.destination.Config.bluecoreNamespace, "[BLUECORE] account namespace required for Authentication.");
+ - name: prepareIdentifyPayload
+ condition: $.outputs.messageType === {{$.EventType.IDENTIFY}}
+ template: |
+ const payload = $.constructProperties(.message);
+ payload.token = .destination.Config.bluecoreNamespace;
+ $.verifyPayload(payload, .message);
+ payload.event = payload.event ?? 'customer_patch';
+ payload.properties.distinct_id = $.populateAccurateDistinctId(payload, .message);
+ $.context.payloads = [$.removeUndefinedAndNullValues(payload)];
+ - name: handleTrackEvent
+ condition: $.outputs.messageType === {{$.EventType.TRACK}}
+ steps:
+ - name: validateInput
+ description: Additional validation for Track events
+ template: |
+ $.assert(.message.event, "event_name could not be mapped. Aborting.")
+ - name: deduceEventNames
+ template: |
+ $.context.deducedEventNameArray = $.deduceTrackEventName(.message.event,.destination.Config)
+ - name: preparePayload
+ template: |
+ const payload = $.constructProperties(.message);
+ $.context.payloads = $.context.deducedEventNameArray@eventName.(
+ const newPayload = $.cloneDeep(payload);
+ newPayload.properties.distinct_id = $.populateAccurateDistinctId(newPayload, ^.message);
+ const temporaryProductArray = newPayload.properties.products ?? $.createProductForStandardEcommEvent(^.message, eventName);
+ newPayload.properties.products = $.normalizeProductArray(temporaryProductArray);
+ newPayload.event = eventName;
+ newPayload.token = ^.destination.Config.bluecoreNamespace;
+ $.verifyPayload(newPayload, ^.message);
+ $.removeUndefinedNullValuesAndEmptyObjectArray(newPayload)
+ )[];
+
+ - name: buildResponse
+ template: |
+ $.context.payloads.(
+ const response = $.defaultRequestConfig();
+ response.body.JSON = .;
+ response.method = "POST";
+ response.endpoint = "https://api.bluecore.com/api/track/mobile/v1";
+ response.headers = {
+ "Content-Type": "application/json"
+ };
+ response
+ )
diff --git a/src/cdk/v2/destinations/bluecore/utils.js b/src/cdk/v2/destinations/bluecore/utils.js
new file mode 100644
index 0000000000..22ec254fe2
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/utils.js
@@ -0,0 +1,250 @@
+const lodash = require('lodash');
+
+const {
+ InstrumentationError,
+ isDefinedAndNotNullAndNotEmpty,
+ getHashFromArrayWithDuplicate,
+ isDefinedAndNotNull,
+ isDefinedNotNullNotEmpty,
+} = require('@rudderstack/integrations-lib');
+const {
+ getFieldValueFromMessage,
+ validateEventName,
+ constructPayload,
+ getDestinationExternalID,
+} = require('../../../../v0/util');
+const { CommonUtils } = require('../../../../util/common');
+const { EVENT_NAME_MAPPING } = require('./config');
+const { EventType } = require('../../../../constants');
+const { MAPPING_CONFIG, CONFIG_CATEGORIES } = require('./config');
+
+/**
+ * Verifies the correctness of payload for different events.
+ *
+ * @param {Object} payload - The payload object containing event information.
+ * @param {Object} message - The message object containing additional information.
+ * @throws {InstrumentationError} - Throws an error if required properties are missing.
+ * @returns {void}
+ */
+const verifyPayload = (payload, message) => {
+ if (
+ message.type === EventType.IDENTIFY &&
+ isDefinedNotNullNotEmpty(message.traits?.action) &&
+ message.traits?.action !== 'identify'
+ ) {
+ throw new InstrumentationError(
+ "[Bluecore] traits.action must be 'identify' for identify action",
+ );
+ }
+ switch (payload.event) {
+ case 'search':
+ if (!payload?.properties?.search_term) {
+ throw new InstrumentationError(
+ '[Bluecore] property:: search_query is required for search event',
+ );
+ }
+ break;
+ case 'purchase':
+ if (!payload?.properties?.order_id) {
+ throw new InstrumentationError(
+ '[Bluecore] property:: order_id is required for purchase event',
+ );
+ }
+ if (!payload?.properties?.total) {
+ throw new InstrumentationError(
+ '[Bluecore] property:: total is required for purchase event',
+ );
+ }
+ if (
+ !isDefinedAndNotNull(payload?.properties?.customer) ||
+ Object.keys(payload.properties.customer).length === 0
+ ) {
+ throw new InstrumentationError(
+ `[Bluecore] property:: No relevant trait to populate customer information, which is required for ${payload.event} event`,
+ );
+ }
+ break;
+ case 'identify':
+ case 'optin':
+ case 'unsubscribe':
+ if (!isDefinedAndNotNullAndNotEmpty(getFieldValueFromMessage(message, 'email'))) {
+ throw new InstrumentationError(
+ `[Bluecore] property:: email is required for ${payload.event} action`,
+ );
+ }
+ if (
+ !isDefinedAndNotNull(payload?.properties?.customer) ||
+ Object.keys(payload.properties.customer).length === 0
+ ) {
+ throw new InstrumentationError(
+ `[Bluecore] property:: No relevant trait to populate customer information, which is required for ${payload.event} action`,
+ );
+ }
+ break;
+ default:
+ break;
+ }
+};
+
+/**
+ * Deduces the track event name based on the provided track event name and configuration.
+ *
+ * @param {string} trackEventName - The track event name to deduce.
+ * @param {object} Config - The configuration object.
+ * @returns {string|array} - The deduced track event name.
+ */
+const deduceTrackEventName = (trackEventName, destConfig) => {
+ let eventName;
+ const { eventsMapping } = destConfig;
+ validateEventName(trackEventName);
+ /*
+ Step 1: Will look for the event name in the eventsMapping array if mapped to a standard bluecore event.
+ and return the corresponding event name if found.
+ */
+ if (eventsMapping.length > 0) {
+ const keyMap = getHashFromArrayWithDuplicate(eventsMapping, 'from', 'to', false);
+ eventName = keyMap[trackEventName];
+ }
+ if (isDefinedAndNotNullAndNotEmpty(eventName)) {
+ const finalEvent = typeof eventName === 'string' ? [eventName] : [...eventName];
+ return finalEvent;
+ }
+
+ /*
+ Step 2: To find if the particular event is amongst the list of standard
+ Rudderstack ecommerce events, used specifically for Bluecore API
+ mappings.
+ */
+
+ const eventMapInfo = EVENT_NAME_MAPPING.find((eventMap) =>
+ eventMap.src.includes(trackEventName.toLowerCase()),
+ );
+ if (isDefinedAndNotNull(eventMapInfo)) {
+ return [eventMapInfo.dest];
+ }
+
+ // Step 3: if nothing matches this is to be considered as a custom event
+ return [trackEventName];
+};
+
+/**
+ * Determines if the given event name is a standard Bluecore event.
+ *
+ * @param {string} eventName - The name of the event to check.
+ * @returns {boolean} - True if the event is a standard Bluecore event, false otherwise.
+ */
+const isStandardBluecoreEvent = (eventName) => {
+ // Return false immediately if eventName is an empty string or falsy
+ if (!eventName) {
+ return false;
+ }
+ // Proceed with the original check if eventName is not empty
+ return !!EVENT_NAME_MAPPING.some((item) => item.dest.includes(eventName));
+};
+
+/**
+ * Adds an array of products to a message.
+ *
+ * @param {object} message - The message object to add the products to.
+ * @param {array|object} products - The array or object of products to add.
+ * @param {string} eventName - The name of the event.
+ * @throws {InstrumentationError} - If the products array is not defined or null.
+ * @returns {array} - The updated product array.
+ */
+const normalizeProductArray = (products) => {
+ let finalProductArray = null;
+ if (isDefinedAndNotNull(products)) {
+ const productArray = CommonUtils.toArray(products);
+ const mappedProductArray = productArray.map(
+ ({ product_id, sku, id, query, order_id, total, ...rest }) => ({
+ id: product_id || sku || id,
+ ...rest,
+ }),
+ );
+ finalProductArray = mappedProductArray;
+ }
+ // if any custom event is not sent with product array, then it should be null
+ return finalProductArray;
+};
+
+/**
+ * Constructs properties based on the given message.
+ *
+ * @param {object} message - The message object.
+ * @returns {object} - The constructed properties object.
+ */
+const constructProperties = (message) => {
+ const commonCategory = CONFIG_CATEGORIES.COMMON;
+ 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);
+ return finalPayload;
+};
+
+/**
+ * Creates a product for a standard e-commerce event.
+ *
+ * @param {Object} properties - The properties of the product.
+ * @param {string} eventName - The name of the event.
+ * @returns {Array|null} - An array containing the properties if the event is a standard Bluecore event and not 'search', otherwise null.
+ */
+const createProductForStandardEcommEvent = (message, eventName) => {
+ const { event, properties } = message;
+ if (event.toLowerCase() === 'order completed' && eventName === 'purchase') {
+ throw new InstrumentationError('[Bluecore]:: products array is required for purchase event');
+ }
+ if (eventName !== 'search' && isStandardBluecoreEvent(eventName)) {
+ return [properties];
+ }
+ return null;
+};
+/**
+ * Function: populateAccurateDistinctId
+ *
+ * Description:
+ * This function is used to populate the accurate distinct ID based on the given payload and message.
+ *
+ * Parameters:
+ * - payload (object): The payload object containing the event and other data.
+ * - message (object): The message object containing the user data.
+ *
+ * Returns:
+ * - distinctId (string): The accurate distinct ID based on the given payload and message.
+ *
+ * Throws:
+ * - InstrumentationError: If the distinct ID could not be set.
+ *
+ */
+const populateAccurateDistinctId = (payload, message) => {
+ const bluecoreExternalId = getDestinationExternalID(message, 'bluecoreExternalId');
+ if (isDefinedAndNotNullAndNotEmpty(bluecoreExternalId)) {
+ return bluecoreExternalId;
+ }
+ let distinctId;
+ if (payload.event === 'identify') {
+ distinctId = getFieldValueFromMessage(message, 'userId');
+ } else {
+ // email is always a more preferred distinct_id
+ distinctId =
+ getFieldValueFromMessage(message, 'email') || getFieldValueFromMessage(message, 'userId');
+ }
+
+ if (!isDefinedAndNotNullAndNotEmpty(distinctId)) {
+ // dev safe. AnonymouId should be always present
+ throw new InstrumentationError(
+ '[Bluecore] property:: distinct_id could not be set. Please provide either email or userId or anonymousId or externalId as distinct_id.',
+ );
+ }
+ return distinctId;
+};
+
+module.exports = {
+ verifyPayload,
+ deduceTrackEventName,
+ normalizeProductArray,
+ isStandardBluecoreEvent,
+ constructProperties,
+ createProductForStandardEcommEvent,
+ populateAccurateDistinctId,
+};
diff --git a/src/cdk/v2/destinations/bluecore/utils.test.js b/src/cdk/v2/destinations/bluecore/utils.test.js
new file mode 100644
index 0000000000..829073bbcc
--- /dev/null
+++ b/src/cdk/v2/destinations/bluecore/utils.test.js
@@ -0,0 +1,403 @@
+const {
+ normalizeProductArray,
+ verifyPayload,
+ isStandardBluecoreEvent,
+ deduceTrackEventName,
+ populateAccurateDistinctId,
+ createProductForStandardEcommEvent,
+} = require('./utils');
+const { InstrumentationError } = require('@rudderstack/integrations-lib');
+
+describe('normalizeProductArray', () => {
+ // Adds an array of products to a message when products array is defined and not null.
+ it('should add an array of products to a message when products array is defined and not null', () => {
+ const products = [
+ { product_id: 1, name: 'Product 1' },
+ { product_id: 2, name: 'Product 2' },
+ ];
+ const eventName = 'purchase';
+
+ const result = normalizeProductArray(products, eventName);
+
+ expect(result).toEqual([
+ { id: 1, name: 'Product 1' },
+ { id: 2, name: 'Product 2' },
+ ]);
+ });
+
+ // Adds a single product object to a message when a single product object is passed.
+ it('should add a single product object to a message when a single product object is passed', () => {
+ const product = { product_id: 1, name: 'Product 1' };
+ const eventName = 'add_to_cart';
+
+ const result = normalizeProductArray(product, eventName);
+ expect(result).toEqual([{ id: 1, name: 'Product 1' }]);
+ });
+
+ it('should not throw an InstrumentationError for a custom event when products array is null', () => {
+ const message = {};
+ const products = null;
+ const eventName = 'custom';
+
+ expect(() => {
+ normalizeProductArray(message, products, eventName);
+ }).toBeNull;
+ });
+});
+
+describe('verifyPayload', () => {
+ // Verify payload for search event with search_term property.
+ it('should verify payload for search event with search_term property', () => {
+ const payload = {
+ event: 'search',
+ properties: {
+ search_term: 'example',
+ },
+ };
+ expect(() => verifyPayload(payload, {})).not.toThrow();
+ });
+
+ // Verify payload for purchase event with order_id and total properties.
+ it('should verify payload for purchase event with order_id and total and customer properties', () => {
+ const payload = {
+ event: 'purchase',
+ properties: {
+ order_id: '123',
+ total: 100,
+ },
+ };
+ expect(() => verifyPayload(payload, {})).toThrow(InstrumentationError);
+ });
+
+ // Verify payload for identify event with email property.
+ it('should verify payload for identify event with email property', () => {
+ const payload = {
+ event: 'identify',
+ properties: {
+ customer: {
+ first_name: 'John',
+ },
+ },
+ };
+ const message = {
+ traits: {
+ email: 'test@example.com',
+ },
+ };
+ expect(() => verifyPayload(payload, message)).not.toThrow();
+ });
+
+ // Verify payload for search event without search_term property, should throw an InstrumentationError.
+ it('should throw an InstrumentationError when verifying payload for search event without search_term property', () => {
+ const payload = {
+ event: 'search',
+ properties: {},
+ };
+ expect(() => verifyPayload(payload, {})).toThrow(InstrumentationError);
+ });
+
+ // Verify payload for purchase event without order_id property, should throw an InstrumentationError.
+ it('should throw an InstrumentationError when verifying payload for purchase event without order_id property', () => {
+ const payload = {
+ event: 'purchase',
+ properties: {
+ total: 100,
+ },
+ };
+ expect(() => verifyPayload(payload, {})).toThrow(InstrumentationError);
+ });
+
+ // Verify payload for purchase event without total property, should throw an InstrumentationError.
+ it('should throw an InstrumentationError when verifying payload for purchase event without total property', () => {
+ const payload = {
+ event: 'purchase',
+ properties: {
+ order_id: '123',
+ },
+ };
+ expect(() => verifyPayload(payload, {})).toThrow(InstrumentationError);
+ });
+
+ // Verify payload for purchase event without total property, should throw an InstrumentationError.
+ it('should throw an InstrumentationError when verifying payload for identify event with action field other than identify', () => {
+ const payload = {
+ event: 'random',
+ properties: {
+ email: 'abc@gmail.com',
+ },
+ };
+ expect(() =>
+ verifyPayload(payload, { type: 'identify', traits: { action: 'random' } }),
+ ).toThrow(InstrumentationError);
+ });
+
+ it('should throw an InstrumentationError when verifying payload for optin event without email property', () => {
+ const payload = {
+ event: 'optin',
+ properties: {
+ order_id: '123',
+ },
+ };
+ expect(() => verifyPayload(payload, {})).toThrow(InstrumentationError);
+ });
+
+ it('should throw an InstrumentationError when verifying payload for unsubscribe event without email property', () => {
+ const payload = {
+ event: 'unsubscribe',
+ properties: {
+ order_id: '123',
+ },
+ };
+ expect(() => verifyPayload(payload, {})).toThrow(InstrumentationError);
+ });
+});
+
+describe('isStandardBluecoreEvent', () => {
+ // Returns true if the given event name is in the list of standard Bluecore events.
+ it('should return true when the given event name is in the list of standard Bluecore events', () => {
+ const eventName = 'search';
+ const result = isStandardBluecoreEvent(eventName);
+ expect(result).toBe(true);
+ });
+
+ // Returns false if the given event name is not in the list of standard Bluecore events.
+ it('should return false when the given event name is not in the list of standard Bluecore events', () => {
+ const eventName = 'someEvent';
+ const result = isStandardBluecoreEvent(eventName);
+ expect(result).toBe(false);
+ });
+
+ // Returns false if the given event name is null.
+ it('should return false when the given event name is null', () => {
+ const eventName = null;
+ const result = isStandardBluecoreEvent(eventName);
+ expect(result).toBe(false);
+ });
+
+ // Returns false if the given event name is undefined.
+ it('should return false when the given event name is undefined', () => {
+ const eventName = undefined;
+ const result = isStandardBluecoreEvent(eventName);
+ expect(result).toBe(false);
+ });
+
+ // Returns false if the given event name is not a string.
+ it('should return false when the given event name is not a string', () => {
+ const eventName = 123;
+ const result = isStandardBluecoreEvent(eventName);
+ expect(result).toBe(false);
+ });
+
+ // Returns false if the given event name is an empty string.
+ it('should return false when the given event name is an empty string', () => {
+ const eventName = '';
+ const result = isStandardBluecoreEvent(eventName);
+ expect(result).toBe(false);
+ });
+});
+
+describe('deduceTrackEventName', () => {
+ // The function returns the trackEventName if no eventsMapping is provided and the trackEventName is not a standard Rudderstack ecommerce event.
+ it('should return the trackEventName when no eventsMapping is provided and the trackEventName is not a standard Rudderstack ecommerce event', () => {
+ const trackEventName = 'customEvent';
+ const Config = {
+ eventsMapping: [],
+ };
+ const result = deduceTrackEventName(trackEventName, Config);
+ expect(result).toEqual([trackEventName]);
+ });
+
+ // The function returns the corresponding event name from eventsMapping if the trackEventName is mapped to a standard bluecore event.
+ it('should return the corresponding event name from eventsMapping if the trackEventName is mapped to a standard bluecore event', () => {
+ const trackEventName = 'customEvent';
+ const Config = {
+ eventsMapping: [{ from: 'customEvent', to: 'search' }],
+ };
+ const result = deduceTrackEventName(trackEventName, Config);
+ expect(result).toEqual(['search']);
+ });
+
+ // The function returns the corresponding event name from eventsMapping if the trackEventName is mapped to a standard bluecore event.
+ it('should return the corresponding event name array from eventsMapping if the trackEventName is mapped to more than one standard bluecore events', () => {
+ const trackEventName = 'customEvent';
+ const Config = {
+ eventsMapping: [
+ { from: 'customEvent', to: 'search' },
+ { from: 'customEvent', to: 'purchase' },
+ ],
+ };
+ const result = deduceTrackEventName(trackEventName, Config);
+ expect(result).toEqual(['search', 'purchase']);
+ });
+
+ // The function returns the corresponding standard Rudderstack ecommerce event name if the trackEventName is a standard bluecore event.
+ it('should return the corresponding standard Rudderstack ecommerce event name if the trackEventName is a standard bluecore event', () => {
+ const trackEventName = 'Product Added to Wishlist';
+ const Config = {
+ eventsMapping: [],
+ };
+ const result = deduceTrackEventName(trackEventName, Config);
+ expect(result).toEqual(['wishlist']);
+ });
+
+ // The function throws an error if the trackEventName is not a string.
+ it('should throw an error if the trackEventName is not a string', () => {
+ const trackEventName = 123;
+ const Config = {
+ eventsMapping: [],
+ };
+ expect(() => deduceTrackEventName(trackEventName, Config)).toThrow();
+ });
+
+ // The function throws an error if the trackEventName is an empty string.
+ it('should throw an error if the trackEventName is an empty string', () => {
+ const trackEventName = '';
+ const Config = {
+ eventsMapping: [],
+ };
+ expect(() => deduceTrackEventName(trackEventName, Config)).toThrow();
+ });
+});
+
+describe('populateAccurateDistinctId', () => {
+ // Returns the distinctId based on the email field when it exists in the message object and the event is not an identify event.
+ it('should return the distinctId based on the email field when it exists in the message object and the event is not an identify event', () => {
+ const payload = { event: 'event' };
+ const message = { userId: '123', context: { traits: { email: 'test@example.com' } } };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('test@example.com');
+ });
+
+ // Returns the distinctId based on the userId field when it exists in the message object and the event is an identify event.
+ it('should return the distinctId based on the userId field when it exists in the message object and the event is an identify event', () => {
+ const payload = { event: 'identify' };
+ const message = { userId: '123', context: { traits: { email: 'test@example.com' } } };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('123');
+ });
+
+ // Returns the distinctId based on the userId field when it exists in the message object and the email field does not exist and the event is not an identify event.
+ it('should return the distinctId based on the userId field when it exists in the message object and the email field does not exist and the event is not an identify event', () => {
+ const payload = { event: 'event' };
+ const message = { userId: '123' };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('123');
+ });
+
+ // Returns the distinctId based on the email field when it exists in the message object and the userId field is empty and the event is not an identify event.
+ it('should throw instrumenatation error as the message is malformed where email is at the root level', () => {
+ const payload = { event: 'event' };
+ const message = { email: 'test@example.com', userId: '' };
+ const testFn = () => populateAccurateDistinctId(payload, message);
+ expect(testFn).toThrow(InstrumentationError);
+ });
+
+ // Returns the distinctId based on the userId field when it exists in the message object and the email field is empty and the event is not an identify event.
+ it('should return the distinctId based on the userId field when it exists in the message object and the email field is empty and the event is not an identify event', () => {
+ const payload = { event: 'event' };
+ const message = { email: '', userId: '123' };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('123');
+ });
+
+ // Returns the distinctId based on the anonymousId field when it exists in the message object and the email and userId fields are empty and the event is not an identify event.
+ it('should return the distinctId based on the anonymousId field when it exists in the message object and the email and userId fields are empty and the event is not an identify event', () => {
+ const payload = { event: 'event' };
+ const message = { anonymousId: 'abc' };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('abc');
+ });
+
+ it('should return the distinctId based on the externalId field when it exists in the context object and the event is not an identify event', () => {
+ const payload = { event: 'event' };
+ const message = {
+ userId: '123',
+ context: {
+ traits: { email: 'test@example.com' },
+ externalId: [{ type: 'bluecoreExternalId', id: '54321' }],
+ },
+ };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('54321');
+ });
+
+ it('should return the distinctId based on the externalId field when it exists in the context object and the event is an identify event', () => {
+ const payload = { event: 'identify' };
+ const message = {
+ userId: '123',
+ context: {
+ traits: { email: 'test@example.com' },
+ externalId: [{ type: 'bluecoreExternalId', id: '54321' }],
+ },
+ };
+ const distinctId = populateAccurateDistinctId(payload, message);
+ expect(distinctId).toBe('54321');
+ });
+});
+
+describe('createProductForStandardEcommEvent', () => {
+ // Returns an array containing the properties if the event is a standard Bluecore event and not 'search'.
+ it("should return an array containing the properties when the event is a standard Bluecore event and not 'search'", () => {
+ const message = {
+ event: 'some event',
+ properties: { name: 'product 1' },
+ };
+ const eventName = 'some event';
+ const result = createProductForStandardEcommEvent(message, eventName);
+ expect(result).toEqual(null);
+ });
+
+ // Returns null if the event is 'search'.
+ it("should return null when the event is 'search'", () => {
+ const message = {
+ event: 'search',
+ properties: { name: 'product 1' },
+ };
+ const eventName = 'search';
+ const result = createProductForStandardEcommEvent(message, eventName);
+ expect(result).toBeNull();
+ });
+
+ // Throws an InstrumentationError if the event is 'order completed' and the eventName is 'purchase'.
+ it("should throw an InstrumentationError when the event is 'order completed' and the eventName is 'purchase'", () => {
+ const message = {
+ event: 'order completed',
+ properties: { name: 'product 1' },
+ };
+ const eventName = 'purchase';
+ expect(() => {
+ createProductForStandardEcommEvent(message, eventName);
+ }).toThrow(InstrumentationError);
+ });
+
+ // Returns null if the eventName is not a standard Bluecore event.
+ it('should return null when the eventName is not a standard Bluecore event', () => {
+ const message = {
+ event: 'some event',
+ properties: { name: 'product 1', products: [{ product_id: 1, name: 'prod1' }] },
+ };
+ const eventName = 'non-standard';
+ const result = createProductForStandardEcommEvent(message, eventName);
+ expect(result).toBeNull();
+ });
+
+ // Returns null if the eventName is not provided.
+ it('should return null when the eventName is not provided', () => {
+ const message = {
+ event: 'some event',
+ properties: { name: 'product 1' },
+ };
+ const result = createProductForStandardEcommEvent(message);
+ expect(result).toBeNull();
+ });
+
+ // Returns null if the properties are not provided.
+ it('should return null when the properties are not provided', () => {
+ const message = {
+ event: 'some event',
+ };
+ const eventName = 'some event';
+ const result = createProductForStandardEcommEvent(message, eventName);
+ expect(result).toBeNull();
+ });
+});
diff --git a/src/cdk/v2/destinations/intercom/utils.js b/src/cdk/v2/destinations/intercom/utils.js
index 0f18029f19..ba3063c9f9 100644
--- a/src/cdk/v2/destinations/intercom/utils.js
+++ b/src/cdk/v2/destinations/intercom/utils.js
@@ -246,11 +246,20 @@ const searchContact = async (message, destination) => {
const headers = getHeaders(destination);
const baseEndPoint = getBaseEndpoint(destination);
const endpoint = `${baseEndPoint}/${SEARCH_CONTACT_ENDPOINT}`;
- const response = await httpPOST(endpoint, data, {
- headers,
- destType: 'intercom',
- feature: 'transformation',
- });
+ const response = await httpPOST(
+ endpoint,
+ data,
+ {
+ headers,
+ },
+ {
+ destType: 'intercom',
+ feature: 'transformation',
+ endpointPath: '/contacts/search',
+ requestMethod: 'POST',
+ module: 'router',
+ },
+ );
const processedUserResponse = processAxiosResponse(response);
if (isHttpStatusSuccess(processedUserResponse.status)) {
return processedUserResponse.response?.data.length > 0
@@ -280,11 +289,20 @@ const createOrUpdateCompany = async (payload, destination) => {
const finalPayload = JSON.stringify(removeUndefinedAndNullValues(payload));
const baseEndPoint = getBaseEndpoint(destination);
const endpoint = `${baseEndPoint}/${CREATE_OR_UPDATE_COMPANY_ENDPOINT}`;
- const response = await httpPOST(endpoint, finalPayload, {
- headers,
- destType: 'intercom',
- feature: 'transformation',
- });
+ const response = await httpPOST(
+ endpoint,
+ finalPayload,
+ {
+ headers,
+ },
+ {
+ destType: 'intercom',
+ feature: 'transformation',
+ endpointPath: '/companies',
+ requestMethod: 'POST',
+ module: 'router',
+ },
+ );
const processedResponse = processAxiosResponse(response);
if (isHttpStatusSuccess(processedResponse.status)) {
diff --git a/src/cdk/v2/destinations/reddit/procWorkflow.yaml b/src/cdk/v2/destinations/reddit/procWorkflow.yaml
index e6c05ef86d..1cf195707d 100644
--- a/src/cdk/v2/destinations/reddit/procWorkflow.yaml
+++ b/src/cdk/v2/destinations/reddit/procWorkflow.yaml
@@ -57,12 +57,14 @@ steps:
- name: customFields
condition: $.outputs.prepareTrackPayload.eventType.tracking_type === "Purchase"
+ 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 customFields = .message.().({
"currency": .properties.currency,
- "value_decimal": .properties.revenue !== undefined ? Number(.properties.revenue) : undefined,
+ "value_decimal": revenue_in_cents ? revenue_in_cents / 100,
"item_count": (Array.isArray(.properties.products) && .properties.products.length) || (.properties.itemCount && Number(.properties.itemCount)),
- "value": .properties.revenue !== undefined ? Number(.properties.revenue)*100 : undefined,
+ "value": revenue_in_cents,
"conversion_id": .properties.conversionId || .messageId,
});
$.removeUndefinedAndNullValues(customFields)
diff --git a/src/cdk/v2/destinations/the_trade_desk/config.js b/src/cdk/v2/destinations/the_trade_desk/config.js
index 828bab3714..300325223b 100644
--- a/src/cdk/v2/destinations/the_trade_desk/config.js
+++ b/src/cdk/v2/destinations/the_trade_desk/config.js
@@ -1,6 +1,4 @@
-const { getMappingConfig } = require('../../../../v0/util');
-
-const SUPPORTED_EVENT_TYPE = ['record', 'track'];
+const SUPPORTED_EVENT_TYPE = ['record'];
const ACTION_TYPES = ['insert', 'delete'];
const DATA_PROVIDER_ID = 'rudderstack';
@@ -15,66 +13,10 @@ const DATA_SERVERS_BASE_ENDPOINTS_MAP = {
china: 'https://data-cn2.adsrvr.cn',
};
-// ref:- https://partner.thetradedesk.com/v3/portal/data/doc/DataConversionEventsApi
-const REAL_TIME_CONVERSION_ENDPOINT = 'https://insight.adsrvr.org/track/realtimeconversion';
-
-const CONVERSION_SUPPORTED_ID_TYPES = [
- 'TDID',
- 'IDFA',
- 'AAID',
- 'DAID',
- 'NAID',
- 'IDL',
- 'EUID',
- 'UID2',
-];
-
-const ECOMM_EVENT_MAP = {
- 'product added': {
- event: 'addtocart',
- rootLevelPriceSupported: true,
- },
- 'order completed': {
- event: 'purchase',
- itemsArray: true,
- revenueFieldSupported: true,
- },
- 'product viewed': {
- event: 'viewitem',
- rootLevelPriceSupported: true,
- },
- 'checkout started': {
- event: 'startcheckout',
- itemsArray: true,
- revenueFieldSupported: true,
- },
- 'cart viewed': {
- event: 'viewcart',
- itemsArray: true,
- },
- 'product added to wishlist': {
- event: 'wishlistitem',
- rootLevelPriceSupported: true,
- },
-};
-
-const CONFIG_CATEGORIES = {
- COMMON_CONFIGS: { name: 'TTDCommonConfig' },
- ITEM_CONFIGS: { name: 'TTDItemConfig' },
-};
-
-const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);
-
module.exports = {
SUPPORTED_EVENT_TYPE,
ACTION_TYPES,
DATA_PROVIDER_ID,
MAX_REQUEST_SIZE_IN_BYTES: 2500000,
DATA_SERVERS_BASE_ENDPOINTS_MAP,
- CONVERSION_SUPPORTED_ID_TYPES,
- CONFIG_CATEGORIES,
- COMMON_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.COMMON_CONFIGS.name],
- ITEM_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.ITEM_CONFIGS.name],
- ECOMM_EVENT_MAP,
- REAL_TIME_CONVERSION_ENDPOINT,
};
diff --git a/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml b/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml
index ee05ecd967..5f0476dd62 100644
--- a/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml
+++ b/src/cdk/v2/destinations/the_trade_desk/rtWorkflow.yaml
@@ -3,20 +3,19 @@ bindings:
path: ../../../../constants
- name: processRecordInputs
path: ./transformRecord
- - name: processConversionInputs
- path: ./transformConversion
- name: handleRtTfSingleEventError
path: ../../../../v0/util/index
- name: InstrumentationError
path: '@rudderstack/integrations-lib'
steps:
- - name: validateCommonConfig
- description: |
- validate common config for first party data and realtime conversion flow
+ - name: validateConfig
template: |
const config = ^[0].destination.Config
+ $.assertConfig(config.audienceId, "Segment name/Audience ID is not present. Aborting")
$.assertConfig(config.advertiserId, "Advertiser ID is not present. Aborting")
+ $.assertConfig(config.advertiserSecretKey, "Advertiser Secret Key is not present. Aborting")
+ config.ttlInDays ? $.assertConfig(config.ttlInDays >=0 && config.ttlInDays <= 180, "TTL is out of range. Allowed values are 0 to 180 days")
- name: validateInput
template: |
@@ -26,22 +25,17 @@ steps:
template: |
$.processRecordInputs(^.{.message.type === $.EventType.RECORD}[], ^[0].destination)
- - name: processConversionEvents
- template: |
- $.processConversionInputs(^.{.message.type === $.EventType.TRACK}[])
-
- name: failOtherEvents
template: |
- const otherEvents = ^.{.message.type !== $.EventType.TRACK && .message.type !== $.EventType.RECORD}[]
+ const otherEvents = ^.{.message.type !== $.EventType.RECORD}[]
let failedEvents = otherEvents.map(
function(event) {
const error = new $.InstrumentationError("Event type " + event.message.type + " is not supported");
$.handleRtTfSingleEventError(event, error, {})
}
)
-
failedEvents ?? []
- name: finalPayload
template: |
- [...$.outputs.processRecordEvents, ...$.outputs.processConversionEvents, ...$.outputs.failOtherEvents]
+ [...$.outputs.processRecordEvents, ...$.outputs.failOtherEvents]
diff --git a/src/cdk/v2/destinations/the_trade_desk/transformConversion.js b/src/cdk/v2/destinations/the_trade_desk/transformConversion.js
deleted file mode 100644
index b282c43151..0000000000
--- a/src/cdk/v2/destinations/the_trade_desk/transformConversion.js
+++ /dev/null
@@ -1,98 +0,0 @@
-const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib');
-const {
- defaultRequestConfig,
- simpleProcessRouterDest,
- defaultPostRequestConfig,
- removeUndefinedAndNullValues,
-} = require('../../../../v0/util');
-const { EventType } = require('../../../../constants');
-const { REAL_TIME_CONVERSION_ENDPOINT } = require('./config');
-const {
- prepareFromConfig,
- prepareCommonPayload,
- getRevenue,
- prepareItemsPayload,
- getAdvertisingId,
- prepareCustomProperties,
- populateEventName,
- getDataProcessingOptions,
- getPrivacySetting,
- enrichTrackPayload,
-} = require('./utils');
-
-const responseBuilder = (payload) => {
- const response = defaultRequestConfig();
- response.endpoint = REAL_TIME_CONVERSION_ENDPOINT;
- response.method = defaultPostRequestConfig.requestMethod;
- response.body.JSON = payload;
- return response;
-};
-
-const validateInputAndConfig = (message, destination) => {
- const { Config } = destination;
- if (!Config.trackerId) {
- throw new ConfigurationError('Tracking Tag ID is not present. Aborting');
- }
-
- if (!message.type) {
- throw new InstrumentationError('Event type is required');
- }
-
- const messageType = message.type.toLowerCase();
- if (messageType !== EventType.TRACK) {
- throw new InstrumentationError(`Event type "${messageType}" is not supported`);
- }
-
- if (!message.event) {
- throw new InstrumentationError('Event name is not present. Aborting.');
- }
-};
-
-const prepareTrackPayload = (message, destination) => {
- const configPayload = prepareFromConfig(destination);
- const commonPayload = prepareCommonPayload(message);
- // prepare items array
- const items = prepareItemsPayload(message);
- const { id, type } = getAdvertisingId(message);
- // get td1-td10 custom properties
- const customProperties = prepareCustomProperties(message, destination);
- const eventName = populateEventName(message, destination);
- const value = getRevenue(message);
- let payload = {
- ...configPayload,
- ...commonPayload,
- event_name: eventName,
- value,
- items,
- adid: id,
- adid_type: type,
- ...customProperties,
- data_processing_option: getDataProcessingOptions(message),
- privacy_settings: getPrivacySetting(message),
- };
-
- payload = enrichTrackPayload(message, payload);
- return { data: [removeUndefinedAndNullValues(payload)] };
-};
-
-const trackResponseBuilder = (message, destination) => {
- const payload = prepareTrackPayload(message, destination);
- return responseBuilder(payload);
-};
-
-const processEvent = (message, destination) => {
- validateInputAndConfig(message, destination);
- return trackResponseBuilder(message, destination);
-};
-
-const process = (event) => processEvent(event.message, event.destination);
-
-const processConversionInputs = async (inputs, reqMetadata) => {
- if (!inputs || inputs.length === 0) {
- return [];
- }
- const respList = await simpleProcessRouterDest(inputs, process, reqMetadata);
- return respList;
-};
-
-module.exports = { processConversionInputs };
diff --git a/src/cdk/v2/destinations/the_trade_desk/transformRecord.js b/src/cdk/v2/destinations/the_trade_desk/transformRecord.js
index d571e11b7a..b452f8d7bc 100644
--- a/src/cdk/v2/destinations/the_trade_desk/transformRecord.js
+++ b/src/cdk/v2/destinations/the_trade_desk/transformRecord.js
@@ -1,4 +1,4 @@
-const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib');
+const { InstrumentationError } = require('@rudderstack/integrations-lib');
const { BatchUtils } = require('@rudderstack/workflow-engine');
const {
defaultPostRequestConfig,
@@ -13,20 +13,6 @@ const tradeDeskConfig = require('./config');
const { DATA_PROVIDER_ID } = tradeDeskConfig;
-const validateConfig = (config) => {
- if (!config.advertiserSecretKey) {
- throw new ConfigurationError('Advertiser Secret Key is not present. Aborting');
- }
-
- if (config.ttlInDays && !(config.ttlInDays >= 0 && config.ttlInDays <= 180)) {
- throw new ConfigurationError('TTL is out of range. Allowed values are 0 to 180 days');
- }
-
- if (!config.audienceId) {
- throw new ConfigurationError('Segment name/Audience ID is not present. Aborting');
- }
-};
-
const responseBuilder = (items, config) => {
const { advertiserId, dataServer } = config;
@@ -64,8 +50,6 @@ const processRecordInputs = (inputs, destination) => {
return [];
}
- validateConfig(Config);
-
const invalidActionTypeError = new InstrumentationError(
'Invalid action type. You can only add or remove IDs from the audience/segment',
);
diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.js b/src/cdk/v2/destinations/the_trade_desk/utils.js
index f51d8dc3ff..71b479438c 100644
--- a/src/cdk/v2/destinations/the_trade_desk/utils.js
+++ b/src/cdk/v2/destinations/the_trade_desk/utils.js
@@ -1,28 +1,10 @@
-const lodash = require('lodash');
-const get = require('get-value');
const CryptoJS = require('crypto-js');
-const { InstrumentationError, AbortedError } = require('@rudderstack/integrations-lib');
-const {
- constructPayload,
- getHashFromArray,
- isDefinedAndNotNull,
- isAppleFamily,
- getIntegrationsObj,
- extractCustomFields,
- generateExclusionList,
-} = require('../../../../v0/util');
-const {
- DATA_SERVERS_BASE_ENDPOINTS_MAP,
- CONVERSION_SUPPORTED_ID_TYPES,
- COMMON_CONFIGS,
- ITEM_CONFIGS,
- ECOMM_EVENT_MAP,
-} = require('./config');
+const { AbortedError } = require('@rudderstack/integrations-lib');
+const { DATA_SERVERS_BASE_ENDPOINTS_MAP } = require('./config');
const getTTLInMin = (ttl) => parseInt(ttl, 10) * 1440;
const getBaseEndpoint = (dataServer) => DATA_SERVERS_BASE_ENDPOINTS_MAP[dataServer];
const getFirstPartyEndpoint = (dataServer) => `${getBaseEndpoint(dataServer)}/data/advertiser`;
-const prepareCommonPayload = (message) => constructPayload(message, COMMON_CONFIGS);
/**
* Generates a signature header for a given request using a secret key.
@@ -41,300 +23,8 @@ const getSignatureHeader = (request, secretKey) => {
return base;
};
-const prepareFromConfig = (destination) => ({
- tracker_id: destination.Config?.trackerId,
- adv: destination.Config?.advertiserId,
-});
-
-/**
- * Calculates the revenue based on the given message.
- *
- * @param {Object} message - The message object containing the event and properties.
- * @returns {number} - The calculated revenue.
- * @throws {InstrumentationError} - If the event is 'Order Completed' and revenue is not provided.
- */
-const getRevenue = (message) => {
- const { event, properties } = message;
- let revenue = properties?.value;
- const eventsMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()];
- if (eventsMapInfo?.rootLevelPriceSupported) {
- const { price, quantity = 1 } = properties;
- if (price && !Number.isNaN(parseFloat(price)) && !Number.isNaN(parseInt(quantity, 10))) {
- revenue = parseFloat(price) * parseInt(quantity, 10);
- }
- } else if (eventsMapInfo?.revenueFieldSupported) {
- revenue = properties?.revenue || revenue;
- if (event.toLowerCase() === 'order completed' && !revenue) {
- throw new InstrumentationError('value is required for `Order Completed` event');
- }
- }
-
- return revenue;
-};
-
-/**
- * Generates items from properties of a given message.
- *
- * @param {Object} message - The message object containing properties.
- * @returns {Array} - An array of items generated from the properties.
- */
-const prepareItemsFromProperties = (message) => {
- const { properties } = message;
- const items = [];
- const item = constructPayload(properties, ITEM_CONFIGS);
- items.push(item);
- return items;
-};
-
-/**
- * Generates items payload from products.
- *
- * @param {Object} message - The message object.
- * @returns {Array} - The items payload.
- */
-const prepareItemsFromProducts = (message) => {
- const products = get(message, 'properties.products');
- const items = [];
- products.forEach((product) => {
- const item = constructPayload(product, ITEM_CONFIGS);
- const itemExclusionList = generateExclusionList(ITEM_CONFIGS);
- extractCustomFields(product, item, 'root', itemExclusionList);
- items.push(item);
- });
- return items;
-};
-
-/**
- * Generates items payload from root properties or products.
- *
- * @param {Object} message - The message object containing event and properties.
- * @returns {Array} - The array of items payload.
- */
-const prepareItemsPayload = (message) => {
- const { event } = message;
- let items;
- const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()];
- if (eventMapInfo?.itemsArray) {
- // if event is one of the supported ecommerce events and products array is present
- items = prepareItemsFromProducts(message);
- } else if (eventMapInfo) {
- // if event is one of the supported ecommerce events and products array is not present
- items = prepareItemsFromProperties(message);
- }
- return items;
-};
-
-/**
- * Retrieves the device advertising ID and type based on the provided message.
- *
- * @param {Object} message - The message object containing the context.
- * @returns {Object} - An object containing the device advertising ID and type.
- */
-const getDeviceAdvertisingId = (message) => {
- const { context } = message;
- const deviceId = context?.device?.advertisingId;
- const osName = context?.os?.name?.toLowerCase();
-
- let type;
- switch (osName) {
- case 'android':
- type = 'AAID';
- break;
- case 'windows':
- type = 'NAID';
- break;
- default:
- type = isAppleFamily(osName) ? 'IDFA' : undefined;
- break;
- }
-
- return { deviceId, type };
-};
-
-/**
- * Retrieves the external ID object from the given message context.
- *
- * @param {Object} message - The message object containing the context.
- * @returns {Object|undefined} - The external ID object, or undefined if not found.
- */
-const getDestinationExternalIDObject = (message) => {
- const { context } = message;
- const externalIdArray = context?.externalId || [];
-
- let externalIdObj;
-
- if (Array.isArray(externalIdArray)) {
- externalIdObj = externalIdArray.find(
- (extIdObj) =>
- CONVERSION_SUPPORTED_ID_TYPES.includes(extIdObj?.type?.toUpperCase()) && extIdObj?.id,
- );
- }
- return externalIdObj;
-};
-
-/**
- * Retrieves the advertising ID and type from the given message.
- *
- * @param {Object} message - The message object containing the context.
- * @returns {Object} - An object containing the advertising ID and type.
- * If the advertising ID and type are found in the device context, they are returned.
- * If not, the external ID object is checked and if found, its ID and type are returned.
- * If neither the device context nor the external ID object contain the required information,
- * an object with null values for ID and type is returned.
- */
-const getAdvertisingId = (message) => {
- const { deviceId, type } = getDeviceAdvertisingId(message);
- if (deviceId && type) {
- return { id: deviceId, type };
- }
- const externalIdObj = getDestinationExternalIDObject(message);
- if (externalIdObj?.id && externalIdObj?.type) {
- return { id: externalIdObj.id, type: externalIdObj.type.toUpperCase() };
- }
-
- return { id: null, type: null };
-};
-
-/**
- * Prepares custom properties (td1-td10) for a given message and destination.
- *
- * @param {object} message - The message object.
- * @param {object} destination - The destination object.
- * @returns {object} - The prepared payload object.
- */
-const prepareCustomProperties = (message, destination) => {
- const { customProperties } = destination.Config;
- const payload = {};
- if (customProperties) {
- customProperties.forEach((customProperty) => {
- const { rudderProperty, tradeDeskProperty } = customProperty;
- const value = get(message, rudderProperty);
- if (value) {
- payload[tradeDeskProperty] = value;
- // unset the rudder property from the message, since it is already mapped to a trade desk property
- lodash.unset(message, rudderProperty);
- }
- });
- }
- return payload;
-};
-
-/**
- * Retrieves the event name based on the provided message and destination.
- *
- * @param {object} message - The message object containing the event.
- * @param {object} destination - The destination object containing the events mapping configuration.
- * @returns {string} - The event name.
- */
-const populateEventName = (message, destination) => {
- let eventName;
- const { event } = message;
- const { eventsMapping } = destination.Config;
-
- // if event is mapped on dashboard, use the mapped event name
- if (Array.isArray(eventsMapping) && eventsMapping.length > 0) {
- const keyMap = getHashFromArray(eventsMapping, 'from', 'to');
- eventName = keyMap[event.toLowerCase()];
- }
-
- if (eventName) {
- return eventName;
- }
-
- // if event is one of the supported ecommerce events, use the mapped trade desk event name
- const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()];
- if (isDefinedAndNotNull(eventMapInfo)) {
- return eventMapInfo.event;
- }
-
- // else return the event name as it is
- return event;
-};
-
-/**
- * Retrieves the data processing options based on the provided message.
- *
- * @param {string} message - The message to process.
- * @throws {InstrumentationError} - Throws an error if the region is not supported, if no policies are provided, if multiple policies are provided, or if the policy is not supported.
- * @returns {Object} - The data processing options, including the policies and region.
- */
-const getDataProcessingOptions = (message) => {
- const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK') || {};
- let { policies } = integrationObj;
- const { region } = integrationObj;
- let dataProcessingOptions;
-
- if (region && !region.toLowerCase().startsWith('us')) {
- throw new InstrumentationError('Only US states are supported');
- }
-
- if (!policies || (Array.isArray(policies) && policies.length === 0)) {
- policies = ['LDU'];
- }
-
- if (policies.length > 1) {
- throw new InstrumentationError('Only one policy is allowed');
- }
-
- if (policies[0] !== 'LDU') {
- throw new InstrumentationError('Only LDU policy is supported');
- }
-
- if (policies && region) {
- dataProcessingOptions = { policies, region };
- }
-
- return dataProcessingOptions;
-};
-
-const getPrivacySetting = (message) => {
- const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK');
- return integrationObj?.privacy_settings;
-};
-
-/**
- * Enriches the track payload with extra properties present in 'properties' other than the ones defined in TTDCommonConfig.json and TTDItemConfig.json
- *
- * @param {Object} message - The message object containing the event information.
- * @param {Object} payload - The payload object to be enriched.
- * @returns {Object} - The enriched payload object.
- */
-const enrichTrackPayload = (message, payload) => {
- let rawPayload = { ...payload };
- const eventsMapInfo = ECOMM_EVENT_MAP[message.event.toLowerCase()];
- // checking if event is an ecomm one and itemsArray/products support is not present. e.g Product Added event
- if (eventsMapInfo && !eventsMapInfo.itemsArray) {
- const itemExclusionList = generateExclusionList(ITEM_CONFIGS);
- rawPayload = extractCustomFields(message, rawPayload, ['properties'], itemExclusionList);
- } else if (eventsMapInfo) {
- // for ecomm events with products array supports. e.g Order Completed event
- rawPayload = extractCustomFields(
- message,
- rawPayload,
- ['properties'],
- ['products', 'revenue', 'value'],
- );
- } else {
- // for custom events
- rawPayload = extractCustomFields(message, rawPayload, ['properties'], ['value']);
- }
- return rawPayload;
-};
-
module.exports = {
getTTLInMin,
getFirstPartyEndpoint,
getSignatureHeader,
- prepareFromConfig,
- getRevenue,
- prepareCommonPayload,
- prepareItemsPayload,
- getDeviceAdvertisingId,
- getDestinationExternalIDObject,
- getAdvertisingId,
- prepareCustomProperties,
- populateEventName,
- getDataProcessingOptions,
- getPrivacySetting,
- enrichTrackPayload,
};
diff --git a/src/cdk/v2/destinations/the_trade_desk/utils.test.js b/src/cdk/v2/destinations/the_trade_desk/utils.test.js
index 029c3004ae..81fd7cf17d 100644
--- a/src/cdk/v2/destinations/the_trade_desk/utils.test.js
+++ b/src/cdk/v2/destinations/the_trade_desk/utils.test.js
@@ -1,16 +1,5 @@
-const { AbortedError, InstrumentationError } = require('@rudderstack/integrations-lib');
-const {
- getSignatureHeader,
- getRevenue,
- getDeviceAdvertisingId,
- getDestinationExternalIDObject,
- getAdvertisingId,
- prepareCustomProperties,
- populateEventName,
- getDataProcessingOptions,
- getPrivacySetting,
- enrichTrackPayload,
-} = require('./utils');
+const { AbortedError } = require('@rudderstack/integrations-lib');
+const { getSignatureHeader } = require('./utils');
describe('getSignatureHeader', () => {
it('should calculate the signature header for a valid request and secret key', () => {
@@ -58,612 +47,3 @@ describe('getSignatureHeader', () => {
}).toThrow(AbortedError);
});
});
-
-describe('getRevenue', () => {
- it('should return revenue value from message properties for custom events', () => {
- const message = {
- event: 'customEvent',
- properties: {
- value: 100,
- },
- };
- const result = getRevenue(message);
- expect(result).toBe(100);
- });
-
- it('should calculate revenue based on price and quantity from message properties if ecomm event is supported for price calculation', () => {
- const message = {
- event: 'Product Added',
- properties: {
- price: 10,
- quantity: 5,
- },
- };
- const result = getRevenue(message);
- expect(result).toBe(50);
- });
-
- it('should return revenue value from message properties if ecomm event is supported for revenue calculation', () => {
- const message = {
- event: 'Order Completed',
- properties: {
- revenue: 200,
- },
- };
- const result = getRevenue(message);
- expect(result).toBe(200);
- });
-
- it('should return default revenue value from properties.value for ecomm events', () => {
- let message = {
- event: 'Product Added',
- properties: {
- price: '',
- value: 200,
- },
- };
- let result = getRevenue(message);
- expect(result).toBe(200);
-
- message = {
- event: 'Order Completed',
- properties: {
- value: 200,
- },
- };
- result = getRevenue(message);
- expect(result).toBe(200);
- });
-
- it('should throw an Instrumentation error if revenue is missing for `Order Completed` event', () => {
- const message = {
- event: 'Order Completed',
- properties: {},
- };
- expect(() => {
- getRevenue(message);
- }).toThrow(InstrumentationError);
- });
-});
-
-describe('getDeviceAdvertisingId', () => {
- it('should return an object with deviceId and type properties when context.device.advertisingId and context.os.name are present', () => {
- let message = {
- context: {
- device: {
- advertisingId: '123456789',
- },
- os: {
- name: 'android',
- },
- },
- };
- let result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: '123456789', type: 'AAID' });
-
- message = {
- context: {
- device: {
- advertisingId: '123456789',
- },
- os: {
- name: 'ios',
- },
- },
- };
- result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: '123456789', type: 'IDFA' });
-
- message = {
- context: {
- device: {
- advertisingId: '123456789',
- },
- os: {
- name: 'windows',
- },
- },
- };
- result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: '123456789', type: 'NAID' });
- });
-
- it('should return an object with undefined type property when osName is not "android", "windows", or an Apple OS', () => {
- const message = {
- context: {
- device: {
- advertisingId: '123456789',
- },
- os: {
- name: 'linux',
- },
- },
- };
- const result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: '123456789', type: undefined });
- });
-
- it('should return an object with undefined deviceId and type properties when context is undefined', () => {
- let message = {};
- let result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: undefined, type: undefined });
-
- message = {
- context: {},
- };
- result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: undefined, type: undefined });
-
- message = {
- context: {
- device: {},
- },
- };
- result = getDeviceAdvertisingId(message);
- expect(result).toEqual({ deviceId: undefined, type: undefined });
- });
-});
-
-describe('getDestinationExternalIDObject', () => {
- it('should return the external ID object when it exists in the message context', () => {
- const message = {
- context: {
- externalId: [
- { id: '123', type: 'daid' },
- { id: '456', type: 'type123' },
- ],
- },
- };
- const result = getDestinationExternalIDObject(message);
- expect(result).toEqual({ id: '123', type: 'daid' });
- });
-
- it('should return undefined when no external ID object exists in the message context', () => {
- let message = {
- context: {
- externalId: [],
- },
- };
- let result = getDestinationExternalIDObject(message);
- expect(result).toBeUndefined();
-
- message = {
- context: {},
- };
- result = getDestinationExternalIDObject(message);
- expect(result).toBeUndefined();
- });
-
- it('should return the first matching external ID object in the array', () => {
- const message = {
- context: {
- externalId: [
- { id: '', type: 'daid' },
- { id: '456', type: 'tdid' },
- { id: '789', type: 'UID2' },
- ],
- },
- };
- const result = getDestinationExternalIDObject(message);
- expect(result).toEqual({ id: '456', type: 'tdid' });
- });
-});
-
-describe('getAdvertisingId', () => {
- it('should return an object with the ID and type when the message contains a valid device advertising ID and OS type', () => {
- const message = {
- context: {
- device: {
- advertisingId: '1234567890',
- },
- os: {
- name: 'android',
- },
- },
- };
-
- const result = getAdvertisingId(message);
- expect(result).toEqual({ id: '1234567890', type: 'AAID' });
- });
-
- it('should return an object with the ID and type when the message contains a valid external ID object with a supported type', () => {
- const message = {
- context: {
- externalId: [
- {
- type: 'IDFA',
- id: 'abcdefg',
- },
- ],
- },
- };
-
- const result = getAdvertisingId(message);
- expect(result).toEqual({ id: 'abcdefg', type: 'IDFA' });
- });
-
- it('should return an object with undefined ID and type when the message contains a valid external ID object with an unsupported type', () => {
- let message = {
- context: {
- externalId: [
- {
- type: 'unsupported',
- id: '1234567890',
- },
- ],
- },
- };
-
- let result = getAdvertisingId(message);
- expect(result).toEqual({ id: null, type: null });
-
- message = {
- context: {
- device: {
- advertisingId: '1234567890',
- },
- },
- };
-
- result = getAdvertisingId(message);
- expect(result).toEqual({ id: null, type: null });
- });
-
- it('should return an object with undefined ID and type when the message contains an external ID object with a supported type but no ID or missing externalId', () => {
- let message = {
- context: {
- externalId: [
- {
- type: 'IDFA',
- },
- ],
- },
- };
- let result = getAdvertisingId(message);
- expect(result).toEqual({ id: null, type: null });
-
- message = {
- context: {},
- };
- result = getAdvertisingId(message);
- expect(result).toEqual({ id: null, type: null });
- });
-});
-
-describe('prepareCustomProperties', () => {
- it('should return an empty object when customProperties is an empty array', () => {
- const message = {};
- let destination = { Config: { customProperties: [] } };
- let result = prepareCustomProperties(message, destination);
- expect(result).toEqual({});
-
- destination = { Config: { customProperties: [{ rudderProperty: '', tradeDeskProperty: '' }] } };
- result = prepareCustomProperties(message, destination);
- expect(result).toEqual({});
-
- destination = { Config: { customProperties: undefined } };
- result = prepareCustomProperties(message, destination);
- expect(result).toEqual({});
- });
-
- it('should return an object with `tradeDeskProperty` as key and `rudderProperty` value as value when `rudderProperty` exists in message', () => {
- const message = {
- rudderProperty1: 'value1',
- rudderProperty2: 'value2',
- };
- const destination = {
- Config: {
- customProperties: [
- {
- rudderProperty: 'rudderProperty1',
- tradeDeskProperty: 'tradeDeskProperty1',
- },
- {
- rudderProperty: 'rudderProperty2',
- tradeDeskProperty: 'tradeDeskProperty2',
- },
- {
- rudderProperty: 'rudderProperty3',
- tradeDeskProperty: 'tradeDeskProperty3',
- },
- ],
- },
- };
- const result = prepareCustomProperties(message, destination);
- expect(result).toEqual({
- tradeDeskProperty1: 'value1',
- tradeDeskProperty2: 'value2',
- });
- });
-});
-
-describe('populateEventName', () => {
- it('should return the eventName if it exists in the eventsMapping of destination.Config', () => {
- const message = { event: 'someEvent' };
- const destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } };
- const result = populateEventName(message, destination);
- expect(result).toBe('mappedEvent');
- });
-
- it('should return the eventName if it exists in the ECOMM_EVENT_MAP', () => {
- const message = { event: 'product added' };
- let destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } };
- let result = populateEventName(message, destination);
- expect(result).toBe('addtocart');
-
- destination = { Config: { eventsMapping: [] } };
- result = populateEventName(message, destination);
- expect(result).toBe('addtocart');
- });
-
- it('should return undefined if eventsMapping is an empty array', () => {
- const message = { event: 'someEvent' };
- const destination = { Config: { eventsMapping: [] } };
- const result = populateEventName(message, destination);
- expect(result).toBe('someEvent');
- });
-});
-
-describe('getDataProcessingOptions', () => {
- it('should return an object with policies and region when provided a integrationObj in message', () => {
- const message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {
- policies: ['LDU'],
- region: 'US-CO',
- },
- },
- };
- const expected = {
- policies: ['LDU'],
- region: 'US-CO',
- };
- const result = getDataProcessingOptions(message);
- expect(result).toEqual(expected);
- });
-
- it('should throw an InstrumentationError if the region is not a US state', () => {
- const message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {
- policies: ['LDU'],
- region: 'EU-abc',
- },
- },
- };
- expect(() => {
- getDataProcessingOptions(message);
- }).toThrow(InstrumentationError);
- });
-
- it('should throw an InstrumentationError if multiple policies are provided', () => {
- const message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {
- policies: ['LDU', 'Policy2'],
- region: 'US-CO',
- },
- },
- };
-
- expect(() => {
- getDataProcessingOptions(message);
- }).toThrow(InstrumentationError);
- });
-
- it('should throw an InstrumentationError if a policy other than `LDU` is provided', () => {
- const message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {
- policies: ['Policy1'],
- region: 'US-CO',
- },
- },
- };
-
- expect(() => {
- getDataProcessingOptions(message);
- }).toThrow(InstrumentationError);
- });
-
- it('should return an object with default policy `LDU` when policies are not provided', () => {
- const message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {
- policies: [],
- region: 'US-CO',
- },
- },
- };
-
- const expected = {
- policies: ['LDU'],
- region: 'US-CO',
- };
-
- expect(getDataProcessingOptions(message)).toEqual(expected);
- });
-
- it('should handle empty cases', () => {
- let message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {},
- },
- };
-
- expect(getDataProcessingOptions(message)).toBeUndefined();
-
- message = {
- integrations: {
- All: true,
- },
- };
-
- expect(getDataProcessingOptions(message)).toBeUndefined();
-
- message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: { region: 'US-CO' },
- },
- };
-
- expect(getDataProcessingOptions(message)).toEqual({ policies: ['LDU'], region: 'US-CO' });
- });
-});
-
-describe('getPrivacySetting', () => {
- it('should return the privacy settings object when it exists in the integration object', () => {
- const message = {
- integrations: {
- All: true,
- THE_TRADE_DESK: {
- privacy_settings: [
- {
- privacy_type: 'GDPR',
- is_applicable: 1,
- consent_string: 'ok',
- },
- ],
- },
- },
- };
- const expected = [
- {
- privacy_type: 'GDPR',
- is_applicable: 1,
- consent_string: 'ok',
- },
- ];
- const result = getPrivacySetting(message);
- expect(result).toEqual(expected);
- });
-
- it('should return null when the privacy settings object does not exist in the integration object', () => {
- let message = { integrations: {} };
- expect(getPrivacySetting(message)).toBeUndefined();
-
- message = { integrations: { THE_TRADE_DESK: {} } };
- expect(getPrivacySetting(message)).toBeUndefined();
-
- message = { integrations: { THE_TRADE_DESK: { privacy_settings: null } } };
- expect(getPrivacySetting(message)).toBeNull();
- });
-});
-
-describe('enrichTrackPayload', () => {
- it('should correctly enrich the payload with custom fields for ecomm events where product array is not supported', () => {
- const message = {
- event: 'Product Added',
- properties: {
- product_id: 'prd123',
- sku: 'sku123',
- brand: 'brand123',
- property1: 'value1',
- property2: 'value2',
- },
- };
- const payload = {
- items: [
- {
- item_code: 'prd123',
- },
- ],
- property1: 'value1',
- property2: 'value2',
- };
- const expectedPayload = {
- items: [
- {
- item_code: 'prd123',
- },
- ],
- brand: 'brand123',
- property1: 'value1',
- property2: 'value2',
- };
-
- const result = enrichTrackPayload(message, payload);
- expect(result).toEqual(expectedPayload);
- });
-
- it('should correctly enrich the payload with custom fields when the for ecomm events with products array support', () => {
- const message = {
- event: 'order completed',
- properties: {
- order_id: 'ord123',
- total: 52.0,
- subtotal: 45.0,
- revenue: 50.0,
- products: [{ product_id: 'prd123', sku: 'sku123', brand: 'brand123' }],
- property1: 'value1',
- property2: 'value2',
- },
- };
- const payload = {
- order_id: 'ord123',
- value: 50.0,
- items: [{ item_code: 'prd123', brand: 'brand123' }],
- property1: 'value1',
- property2: 'value2',
- };
- const expectedPayload = {
- order_id: 'ord123',
- total: 52.0,
- subtotal: 45.0,
- value: 50.0,
- items: [{ item_code: 'prd123', brand: 'brand123' }],
- property1: 'value1',
- property2: 'value2',
- };
-
- const result = enrichTrackPayload(message, payload);
-
- expect(result).toEqual(expectedPayload);
- });
-
- it('should return the enriched payload for custom event', () => {
- const message = {
- event: 'someEvent',
- properties: {
- order_id: 'ord123',
- property1: 'value1',
- property2: 'value2',
- revenue: 10,
- value: 11,
- products: [
- {
- product_id: 'prd123',
- test: 'test',
- },
- ],
- },
- };
- const payload = {
- order_id: 'ord123',
- value: 11,
- };
- const expectedPayload = {
- order_id: 'ord123',
- property1: 'value1',
- property2: 'value2',
- revenue: 10,
- value: 11,
- products: [
- {
- product_id: 'prd123',
- test: 'test',
- },
- ],
- };
-
- const result = enrichTrackPayload(message, payload);
- expect(result).toEqual(expectedPayload);
- });
-});
diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/config.js b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/config.js
new file mode 100644
index 0000000000..7af732185f
--- /dev/null
+++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/config.js
@@ -0,0 +1,63 @@
+const { getMappingConfig } = require('../../../../v0/util');
+
+const SUPPORTED_EVENT_TYPE = ['track'];
+
+// ref:- https://partner.thetradedesk.com/v3/portal/data/doc/DataConversionEventsApi
+const REAL_TIME_CONVERSION_ENDPOINT = 'https://insight.adsrvr.org/track/realtimeconversion';
+
+const CONVERSION_SUPPORTED_ID_TYPES = [
+ 'TDID',
+ 'IDFA',
+ 'AAID',
+ 'DAID',
+ 'NAID',
+ 'IDL',
+ 'EUID',
+ 'UID2',
+];
+
+const ECOMM_EVENT_MAP = {
+ 'product added': {
+ event: 'addtocart',
+ rootLevelPriceSupported: true,
+ },
+ 'order completed': {
+ event: 'purchase',
+ itemsArray: true,
+ revenueFieldSupported: true,
+ },
+ 'product viewed': {
+ event: 'viewitem',
+ rootLevelPriceSupported: true,
+ },
+ 'checkout started': {
+ event: 'startcheckout',
+ itemsArray: true,
+ revenueFieldSupported: true,
+ },
+ 'cart viewed': {
+ event: 'viewcart',
+ itemsArray: true,
+ },
+ 'product added to wishlist': {
+ event: 'wishlistitem',
+ rootLevelPriceSupported: true,
+ },
+};
+
+const CONFIG_CATEGORIES = {
+ COMMON_CONFIGS: { name: 'TTDCommonConfig' },
+ ITEM_CONFIGS: { name: 'TTDItemConfig' },
+};
+
+const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);
+
+module.exports = {
+ SUPPORTED_EVENT_TYPE,
+ CONFIG_CATEGORIES,
+ CONVERSION_SUPPORTED_ID_TYPES,
+ COMMON_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.COMMON_CONFIGS.name],
+ ITEM_CONFIGS: MAPPING_CONFIG[CONFIG_CATEGORIES.ITEM_CONFIGS.name],
+ ECOMM_EVENT_MAP,
+ REAL_TIME_CONVERSION_ENDPOINT,
+};
diff --git a/src/cdk/v2/destinations/the_trade_desk/data/TTDCommonConfig.json b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDCommonConfig.json
similarity index 100%
rename from src/cdk/v2/destinations/the_trade_desk/data/TTDCommonConfig.json
rename to src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDCommonConfig.json
diff --git a/src/cdk/v2/destinations/the_trade_desk/data/TTDItemConfig.json b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDItemConfig.json
similarity index 100%
rename from src/cdk/v2/destinations/the_trade_desk/data/TTDItemConfig.json
rename to src/cdk/v2/destinations/the_trade_desk_real_time_conversions/data/TTDItemConfig.json
diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/procWorkflow.yaml b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/procWorkflow.yaml
new file mode 100644
index 0000000000..5191320cdc
--- /dev/null
+++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/procWorkflow.yaml
@@ -0,0 +1,59 @@
+bindings:
+ - name: EventType
+ path: ../../../../constants
+ - path: ../../bindings/jsontemplate
+ - name: defaultRequestConfig
+ path: ../../../../v0/util
+ - name: removeUndefinedAndNullValues
+ path: ../../../../v0/util
+ - path: ./config
+ exportAll: true
+ - path: ./utils
+ exportAll: true
+
+steps:
+ - name: validateConfig
+ template: |
+ $.assertConfig(.destination.Config.advertiserId, "Advertiser ID is not present. Aborting")
+ $.assertConfig(.destination.Config.trackerId, "Tracking Tag ID is not present. Aborting")
+
+ - name: validateInput
+ template: |
+ let messageType = .message.type;
+ $.assert(messageType, "message Type is not present. Aborting.");
+ $.assert(messageType.toLowerCase() === $.EventType.TRACK, "Event type " + messageType + " is not supported");
+ $.assert(.message.event, "Event is not present. Aborting.");
+
+ - name: prepareTrackPayload
+ template: |
+ const configPayload = $.prepareFromConfig(.destination);
+ const commonPayload = $.prepareCommonPayload(.message);
+ const { id, type } = $.getAdvertisingId(.message);
+ const items = $.prepareItemsPayload(.message);
+ const customProperties = $.prepareCustomProperties(.message, .destination);
+ const eventName = $.populateEventName(.message, .destination);
+ const value = $.getRevenue(.message);
+ let payload = {
+ ...configPayload,
+ ...commonPayload,
+ event_name: eventName,
+ value,
+ items,
+ adid: id,
+ adid_type: type,
+ ...customProperties,
+ data_processing_option: $.getDataProcessingOptions(.message),
+ privacy_settings: $.getPrivacySetting(.message),
+ };
+ payload = $.enrichTrackPayload(.message, payload);
+ payload;
+
+ - name: buildResponseForProcessTransformation
+ template: |
+ const response = $.defaultRequestConfig();
+ response.body.JSON = {data: [$.removeUndefinedAndNullValues($.outputs.prepareTrackPayload)]};
+ response.endpoint = $.REAL_TIME_CONVERSION_ENDPOINT;
+ response.headers = {
+ "Content-Type": "application/json"
+ };
+ response;
diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.js b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.js
new file mode 100644
index 0000000000..2232be61f0
--- /dev/null
+++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.js
@@ -0,0 +1,315 @@
+const lodash = require('lodash');
+const get = require('get-value');
+const { InstrumentationError } = require('@rudderstack/integrations-lib');
+const {
+ constructPayload,
+ getHashFromArray,
+ isDefinedAndNotNull,
+ isAppleFamily,
+ getIntegrationsObj,
+ extractCustomFields,
+ generateExclusionList,
+} = require('../../../../v0/util');
+const {
+ CONVERSION_SUPPORTED_ID_TYPES,
+ COMMON_CONFIGS,
+ ITEM_CONFIGS,
+ ECOMM_EVENT_MAP,
+} = require('./config');
+
+const prepareCommonPayload = (message) => constructPayload(message, COMMON_CONFIGS);
+
+const prepareFromConfig = (destination) => ({
+ tracker_id: destination.Config?.trackerId,
+ adv: destination.Config?.advertiserId,
+});
+
+/**
+ * Calculates the revenue based on the given message.
+ *
+ * @param {Object} message - The message object containing the event and properties.
+ * @returns {number} - The calculated revenue.
+ * @throws {InstrumentationError} - If the event is 'Order Completed' and revenue is not provided.
+ */
+const getRevenue = (message) => {
+ const { event, properties } = message;
+ let revenue = properties?.value;
+ const eventsMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()];
+ if (eventsMapInfo?.rootLevelPriceSupported) {
+ const { price, quantity = 1 } = properties;
+ if (price && !Number.isNaN(parseFloat(price)) && !Number.isNaN(parseInt(quantity, 10))) {
+ revenue = parseFloat(price) * parseInt(quantity, 10);
+ }
+ } else if (eventsMapInfo?.revenueFieldSupported) {
+ revenue = properties?.revenue || revenue;
+ if (event.toLowerCase() === 'order completed' && !revenue) {
+ throw new InstrumentationError('value is required for `Order Completed` event');
+ }
+ }
+
+ return revenue;
+};
+
+/**
+ * Generates items from properties of a given message.
+ *
+ * @param {Object} message - The message object containing properties.
+ * @returns {Array} - An array of items generated from the properties.
+ */
+const prepareItemsFromProperties = (message) => {
+ const { properties } = message;
+ const items = [];
+ const item = constructPayload(properties, ITEM_CONFIGS);
+ items.push(item);
+ return items;
+};
+
+/**
+ * Generates items payload from products.
+ *
+ * @param {Object} message - The message object.
+ * @returns {Array} - The items payload.
+ */
+const prepareItemsFromProducts = (message) => {
+ const products = get(message, 'properties.products');
+ const items = [];
+ products.forEach((product) => {
+ const item = constructPayload(product, ITEM_CONFIGS);
+ const itemExclusionList = generateExclusionList(ITEM_CONFIGS);
+ extractCustomFields(product, item, 'root', itemExclusionList);
+ items.push(item);
+ });
+ return items;
+};
+
+/**
+ * Generates items payload from root properties or products.
+ *
+ * @param {Object} message - The message object containing event and properties.
+ * @returns {Array} - The array of items payload.
+ */
+const prepareItemsPayload = (message) => {
+ const { event } = message;
+ let items;
+ const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()];
+ if (eventMapInfo?.itemsArray) {
+ // if event is one of the supported ecommerce events and products array is present
+ items = prepareItemsFromProducts(message);
+ } else if (eventMapInfo) {
+ // if event is one of the supported ecommerce events and products array is not present
+ items = prepareItemsFromProperties(message);
+ }
+ return items;
+};
+
+/**
+ * Retrieves the device advertising ID and type based on the provided message.
+ *
+ * @param {Object} message - The message object containing the context.
+ * @returns {Object} - An object containing the device advertising ID and type.
+ */
+const getDeviceAdvertisingId = (message) => {
+ const { context } = message;
+ const deviceId = context?.device?.advertisingId;
+ const osName = context?.os?.name?.toLowerCase();
+
+ let type;
+ switch (osName) {
+ case 'android':
+ type = 'AAID';
+ break;
+ case 'windows':
+ type = 'NAID';
+ break;
+ default:
+ type = isAppleFamily(osName) ? 'IDFA' : undefined;
+ break;
+ }
+
+ return { deviceId, type };
+};
+
+/**
+ * Retrieves the external ID object from the given message context.
+ *
+ * @param {Object} message - The message object containing the context.
+ * @returns {Object|undefined} - The external ID object, or undefined if not found.
+ */
+const getDestinationExternalIDObject = (message) => {
+ const { context } = message;
+ const externalIdArray = context?.externalId || [];
+
+ let externalIdObj;
+
+ if (Array.isArray(externalIdArray)) {
+ externalIdObj = externalIdArray.find(
+ (extIdObj) =>
+ CONVERSION_SUPPORTED_ID_TYPES.includes(extIdObj?.type?.toUpperCase()) && extIdObj?.id,
+ );
+ }
+ return externalIdObj;
+};
+
+/**
+ * Retrieves the advertising ID and type from the given message.
+ *
+ * @param {Object} message - The message object containing the context.
+ * @returns {Object} - An object containing the advertising ID and type.
+ * If the advertising ID and type are found in the device context, they are returned.
+ * If not, the external ID object is checked and if found, its ID and type are returned.
+ * If neither the device context nor the external ID object contain the required information,
+ * an object with null values for ID and type is returned.
+ */
+const getAdvertisingId = (message) => {
+ const { deviceId, type } = getDeviceAdvertisingId(message);
+ if (deviceId && type) {
+ return { id: deviceId, type };
+ }
+ const externalIdObj = getDestinationExternalIDObject(message);
+ if (externalIdObj?.id && externalIdObj?.type) {
+ return { id: externalIdObj.id, type: externalIdObj.type.toUpperCase() };
+ }
+
+ return { id: null, type: null };
+};
+
+/**
+ * Prepares custom properties (td1-td10) for a given message and destination.
+ *
+ * @param {object} message - The message object.
+ * @param {object} destination - The destination object.
+ * @returns {object} - The prepared payload object.
+ */
+const prepareCustomProperties = (message, destination) => {
+ const { customProperties } = destination.Config;
+ const payload = {};
+ if (customProperties) {
+ customProperties.forEach((customProperty) => {
+ const { rudderProperty, tradeDeskProperty } = customProperty;
+ const value = get(message, rudderProperty);
+ if (value) {
+ payload[tradeDeskProperty] = value;
+ // unset the rudder property from the message, since it is already mapped to a trade desk property
+ lodash.unset(message, rudderProperty);
+ }
+ });
+ }
+ return payload;
+};
+
+/**
+ * Retrieves the event name based on the provided message and destination.
+ *
+ * @param {object} message - The message object containing the event.
+ * @param {object} destination - The destination object containing the events mapping configuration.
+ * @returns {string} - The event name.
+ */
+const populateEventName = (message, destination) => {
+ let eventName;
+ const { event } = message;
+ const { eventsMapping } = destination.Config;
+
+ // if event is mapped on dashboard, use the mapped event name
+ if (Array.isArray(eventsMapping) && eventsMapping.length > 0) {
+ const keyMap = getHashFromArray(eventsMapping, 'from', 'to');
+ eventName = keyMap[event.toLowerCase()];
+ }
+
+ if (eventName) {
+ return eventName;
+ }
+
+ // if event is one of the supported ecommerce events, use the mapped trade desk event name
+ const eventMapInfo = ECOMM_EVENT_MAP[event.toLowerCase()];
+ if (isDefinedAndNotNull(eventMapInfo)) {
+ return eventMapInfo.event;
+ }
+
+ // else return the event name as it is
+ return event;
+};
+
+/**
+ * Retrieves the data processing options based on the provided message.
+ *
+ * @param {string} message - The message to process.
+ * @throws {InstrumentationError} - Throws an error if the region is not supported, if no policies are provided, if multiple policies are provided, or if the policy is not supported.
+ * @returns {Object} - The data processing options, including the policies and region.
+ */
+const getDataProcessingOptions = (message) => {
+ const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK') || {};
+ let { policies } = integrationObj;
+ const { region } = integrationObj;
+ let dataProcessingOptions;
+
+ if (region && !region.toLowerCase().startsWith('us')) {
+ throw new InstrumentationError('Only US states are supported');
+ }
+
+ if (!policies || (Array.isArray(policies) && policies.length === 0)) {
+ policies = ['LDU'];
+ }
+
+ if (policies.length > 1) {
+ throw new InstrumentationError('Only one policy is allowed');
+ }
+
+ if (policies[0] !== 'LDU') {
+ throw new InstrumentationError('Only LDU policy is supported');
+ }
+
+ if (policies && region) {
+ dataProcessingOptions = { policies, region };
+ }
+
+ return dataProcessingOptions;
+};
+
+const getPrivacySetting = (message) => {
+ const integrationObj = getIntegrationsObj(message, 'THE_TRADE_DESK');
+ return integrationObj?.privacy_settings;
+};
+
+/**
+ * Enriches the track payload with extra properties present in 'properties' other than the ones defined in TTDCommonConfig.json and TTDItemConfig.json
+ *
+ * @param {Object} message - The message object containing the event information.
+ * @param {Object} payload - The payload object to be enriched.
+ * @returns {Object} - The enriched payload object.
+ */
+const enrichTrackPayload = (message, payload) => {
+ let rawPayload = { ...payload };
+ const eventsMapInfo = ECOMM_EVENT_MAP[message.event.toLowerCase()];
+ // checking if event is an ecomm one and itemsArray/products support is not present. e.g Product Added event
+ if (eventsMapInfo && !eventsMapInfo.itemsArray) {
+ const itemExclusionList = generateExclusionList(ITEM_CONFIGS);
+ rawPayload = extractCustomFields(message, rawPayload, ['properties'], itemExclusionList);
+ } else if (eventsMapInfo) {
+ // for ecomm events with products array supports. e.g Order Completed event
+ rawPayload = extractCustomFields(
+ message,
+ rawPayload,
+ ['properties'],
+ ['products', 'revenue', 'value'],
+ );
+ } else {
+ // for custom events
+ rawPayload = extractCustomFields(message, rawPayload, ['properties'], ['value']);
+ }
+ return rawPayload;
+};
+
+module.exports = {
+ prepareFromConfig,
+ getRevenue,
+ prepareCommonPayload,
+ prepareItemsPayload,
+ getDeviceAdvertisingId,
+ getDestinationExternalIDObject,
+ getAdvertisingId,
+ prepareCustomProperties,
+ populateEventName,
+ getDataProcessingOptions,
+ getPrivacySetting,
+ enrichTrackPayload,
+};
diff --git a/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.test.js b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.test.js
new file mode 100644
index 0000000000..20b39fab93
--- /dev/null
+++ b/src/cdk/v2/destinations/the_trade_desk_real_time_conversions/utils.test.js
@@ -0,0 +1,621 @@
+const { InstrumentationError } = require('@rudderstack/integrations-lib');
+const {
+ getRevenue,
+ getDeviceAdvertisingId,
+ getDestinationExternalIDObject,
+ getAdvertisingId,
+ prepareCustomProperties,
+ populateEventName,
+ getDataProcessingOptions,
+ getPrivacySetting,
+ enrichTrackPayload,
+} = require('./utils');
+
+describe('getRevenue', () => {
+ it('should return revenue value from message properties for custom events', () => {
+ const message = {
+ event: 'customEvent',
+ properties: {
+ value: 100,
+ },
+ };
+ const result = getRevenue(message);
+ expect(result).toBe(100);
+ });
+
+ it('should calculate revenue based on price and quantity from message properties if ecomm event is supported for price calculation', () => {
+ const message = {
+ event: 'Product Added',
+ properties: {
+ price: 10,
+ quantity: 5,
+ },
+ };
+ const result = getRevenue(message);
+ expect(result).toBe(50);
+ });
+
+ it('should return revenue value from message properties if ecomm event is supported for revenue calculation', () => {
+ const message = {
+ event: 'Order Completed',
+ properties: {
+ revenue: 200,
+ },
+ };
+ const result = getRevenue(message);
+ expect(result).toBe(200);
+ });
+
+ it('should return default revenue value from properties.value for ecomm events', () => {
+ let message = {
+ event: 'Product Added',
+ properties: {
+ price: '',
+ value: 200,
+ },
+ };
+ let result = getRevenue(message);
+ expect(result).toBe(200);
+
+ message = {
+ event: 'Order Completed',
+ properties: {
+ value: 200,
+ },
+ };
+ result = getRevenue(message);
+ expect(result).toBe(200);
+ });
+
+ it('should throw an Instrumentation error if revenue is missing for `Order Completed` event', () => {
+ const message = {
+ event: 'Order Completed',
+ properties: {},
+ };
+ expect(() => {
+ getRevenue(message);
+ }).toThrow(InstrumentationError);
+ });
+});
+
+describe('getDeviceAdvertisingId', () => {
+ it('should return an object with deviceId and type properties when context.device.advertisingId and context.os.name are present', () => {
+ let message = {
+ context: {
+ device: {
+ advertisingId: '123456789',
+ },
+ os: {
+ name: 'android',
+ },
+ },
+ };
+ let result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: '123456789', type: 'AAID' });
+
+ message = {
+ context: {
+ device: {
+ advertisingId: '123456789',
+ },
+ os: {
+ name: 'ios',
+ },
+ },
+ };
+ result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: '123456789', type: 'IDFA' });
+
+ message = {
+ context: {
+ device: {
+ advertisingId: '123456789',
+ },
+ os: {
+ name: 'windows',
+ },
+ },
+ };
+ result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: '123456789', type: 'NAID' });
+ });
+
+ it('should return an object with undefined type property when osName is not "android", "windows", or an Apple OS', () => {
+ const message = {
+ context: {
+ device: {
+ advertisingId: '123456789',
+ },
+ os: {
+ name: 'linux',
+ },
+ },
+ };
+ const result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: '123456789', type: undefined });
+ });
+
+ it('should return an object with undefined deviceId and type properties when context is undefined', () => {
+ let message = {};
+ let result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: undefined, type: undefined });
+
+ message = {
+ context: {},
+ };
+ result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: undefined, type: undefined });
+
+ message = {
+ context: {
+ device: {},
+ },
+ };
+ result = getDeviceAdvertisingId(message);
+ expect(result).toEqual({ deviceId: undefined, type: undefined });
+ });
+});
+
+describe('getDestinationExternalIDObject', () => {
+ it('should return the external ID object when it exists in the message context', () => {
+ const message = {
+ context: {
+ externalId: [
+ { id: '123', type: 'daid' },
+ { id: '456', type: 'type123' },
+ ],
+ },
+ };
+ const result = getDestinationExternalIDObject(message);
+ expect(result).toEqual({ id: '123', type: 'daid' });
+ });
+
+ it('should return undefined when no external ID object exists in the message context', () => {
+ let message = {
+ context: {
+ externalId: [],
+ },
+ };
+ let result = getDestinationExternalIDObject(message);
+ expect(result).toBeUndefined();
+
+ message = {
+ context: {},
+ };
+ result = getDestinationExternalIDObject(message);
+ expect(result).toBeUndefined();
+ });
+
+ it('should return the first matching external ID object in the array', () => {
+ const message = {
+ context: {
+ externalId: [
+ { id: '', type: 'daid' },
+ { id: '456', type: 'tdid' },
+ { id: '789', type: 'UID2' },
+ ],
+ },
+ };
+ const result = getDestinationExternalIDObject(message);
+ expect(result).toEqual({ id: '456', type: 'tdid' });
+ });
+});
+
+describe('getAdvertisingId', () => {
+ it('should return an object with the ID and type when the message contains a valid device advertising ID and OS type', () => {
+ const message = {
+ context: {
+ device: {
+ advertisingId: '1234567890',
+ },
+ os: {
+ name: 'android',
+ },
+ },
+ };
+
+ const result = getAdvertisingId(message);
+ expect(result).toEqual({ id: '1234567890', type: 'AAID' });
+ });
+
+ it('should return an object with the ID and type when the message contains a valid external ID object with a supported type', () => {
+ const message = {
+ context: {
+ externalId: [
+ {
+ type: 'IDFA',
+ id: 'abcdefg',
+ },
+ ],
+ },
+ };
+
+ const result = getAdvertisingId(message);
+ expect(result).toEqual({ id: 'abcdefg', type: 'IDFA' });
+ });
+
+ it('should return an object with undefined ID and type when the message contains a valid external ID object with an unsupported type', () => {
+ let message = {
+ context: {
+ externalId: [
+ {
+ type: 'unsupported',
+ id: '1234567890',
+ },
+ ],
+ },
+ };
+
+ let result = getAdvertisingId(message);
+ expect(result).toEqual({ id: null, type: null });
+
+ message = {
+ context: {
+ device: {
+ advertisingId: '1234567890',
+ },
+ },
+ };
+
+ result = getAdvertisingId(message);
+ expect(result).toEqual({ id: null, type: null });
+ });
+
+ it('should return an object with undefined ID and type when the message contains an external ID object with a supported type but no ID or missing externalId', () => {
+ let message = {
+ context: {
+ externalId: [
+ {
+ type: 'IDFA',
+ },
+ ],
+ },
+ };
+ let result = getAdvertisingId(message);
+ expect(result).toEqual({ id: null, type: null });
+
+ message = {
+ context: {},
+ };
+ result = getAdvertisingId(message);
+ expect(result).toEqual({ id: null, type: null });
+ });
+});
+
+describe('prepareCustomProperties', () => {
+ it('should return an empty object when customProperties is an empty array', () => {
+ const message = {};
+ let destination = { Config: { customProperties: [] } };
+ let result = prepareCustomProperties(message, destination);
+ expect(result).toEqual({});
+
+ destination = { Config: { customProperties: [{ rudderProperty: '', tradeDeskProperty: '' }] } };
+ result = prepareCustomProperties(message, destination);
+ expect(result).toEqual({});
+
+ destination = { Config: { customProperties: undefined } };
+ result = prepareCustomProperties(message, destination);
+ expect(result).toEqual({});
+ });
+
+ it('should return an object with `tradeDeskProperty` as key and `rudderProperty` value as value when `rudderProperty` exists in message', () => {
+ const message = {
+ rudderProperty1: 'value1',
+ rudderProperty2: 'value2',
+ };
+ const destination = {
+ Config: {
+ customProperties: [
+ {
+ rudderProperty: 'rudderProperty1',
+ tradeDeskProperty: 'tradeDeskProperty1',
+ },
+ {
+ rudderProperty: 'rudderProperty2',
+ tradeDeskProperty: 'tradeDeskProperty2',
+ },
+ {
+ rudderProperty: 'rudderProperty3',
+ tradeDeskProperty: 'tradeDeskProperty3',
+ },
+ ],
+ },
+ };
+ const result = prepareCustomProperties(message, destination);
+ expect(result).toEqual({
+ tradeDeskProperty1: 'value1',
+ tradeDeskProperty2: 'value2',
+ });
+ });
+});
+
+describe('populateEventName', () => {
+ it('should return the eventName if it exists in the eventsMapping of destination.Config', () => {
+ const message = { event: 'someEvent' };
+ const destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } };
+ const result = populateEventName(message, destination);
+ expect(result).toBe('mappedEvent');
+ });
+
+ it('should return the eventName if it exists in the ECOMM_EVENT_MAP', () => {
+ const message = { event: 'product added' };
+ let destination = { Config: { eventsMapping: [{ from: 'someEvent', to: 'mappedEvent' }] } };
+ let result = populateEventName(message, destination);
+ expect(result).toBe('addtocart');
+
+ destination = { Config: { eventsMapping: [] } };
+ result = populateEventName(message, destination);
+ expect(result).toBe('addtocart');
+ });
+
+ it('should return undefined if eventsMapping is an empty array', () => {
+ const message = { event: 'someEvent' };
+ const destination = { Config: { eventsMapping: [] } };
+ const result = populateEventName(message, destination);
+ expect(result).toBe('someEvent');
+ });
+});
+
+describe('getDataProcessingOptions', () => {
+ it('should return an object with policies and region when provided a integrationObj in message', () => {
+ const message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {
+ policies: ['LDU'],
+ region: 'US-CO',
+ },
+ },
+ };
+ const expected = {
+ policies: ['LDU'],
+ region: 'US-CO',
+ };
+ const result = getDataProcessingOptions(message);
+ expect(result).toEqual(expected);
+ });
+
+ it('should throw an InstrumentationError if the region is not a US state', () => {
+ const message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {
+ policies: ['LDU'],
+ region: 'EU-abc',
+ },
+ },
+ };
+ expect(() => {
+ getDataProcessingOptions(message);
+ }).toThrow(InstrumentationError);
+ });
+
+ it('should throw an InstrumentationError if multiple policies are provided', () => {
+ const message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {
+ policies: ['LDU', 'Policy2'],
+ region: 'US-CO',
+ },
+ },
+ };
+
+ expect(() => {
+ getDataProcessingOptions(message);
+ }).toThrow(InstrumentationError);
+ });
+
+ it('should throw an InstrumentationError if a policy other than `LDU` is provided', () => {
+ const message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {
+ policies: ['Policy1'],
+ region: 'US-CO',
+ },
+ },
+ };
+
+ expect(() => {
+ getDataProcessingOptions(message);
+ }).toThrow(InstrumentationError);
+ });
+
+ it('should return an object with default policy `LDU` when policies are not provided', () => {
+ const message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {
+ policies: [],
+ region: 'US-CO',
+ },
+ },
+ };
+
+ const expected = {
+ policies: ['LDU'],
+ region: 'US-CO',
+ };
+
+ expect(getDataProcessingOptions(message)).toEqual(expected);
+ });
+
+ it('should handle empty cases', () => {
+ let message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {},
+ },
+ };
+
+ expect(getDataProcessingOptions(message)).toBeUndefined();
+
+ message = {
+ integrations: {
+ All: true,
+ },
+ };
+
+ expect(getDataProcessingOptions(message)).toBeUndefined();
+
+ message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: { region: 'US-CO' },
+ },
+ };
+
+ expect(getDataProcessingOptions(message)).toEqual({ policies: ['LDU'], region: 'US-CO' });
+ });
+});
+
+describe('getPrivacySetting', () => {
+ it('should return the privacy settings object when it exists in the integration object', () => {
+ const message = {
+ integrations: {
+ All: true,
+ THE_TRADE_DESK: {
+ privacy_settings: [
+ {
+ privacy_type: 'GDPR',
+ is_applicable: 1,
+ consent_string: 'ok',
+ },
+ ],
+ },
+ },
+ };
+ const expected = [
+ {
+ privacy_type: 'GDPR',
+ is_applicable: 1,
+ consent_string: 'ok',
+ },
+ ];
+ const result = getPrivacySetting(message);
+ expect(result).toEqual(expected);
+ });
+
+ it('should return null when the privacy settings object does not exist in the integration object', () => {
+ let message = { integrations: {} };
+ expect(getPrivacySetting(message)).toBeUndefined();
+
+ message = { integrations: { THE_TRADE_DESK: {} } };
+ expect(getPrivacySetting(message)).toBeUndefined();
+
+ message = { integrations: { THE_TRADE_DESK: { privacy_settings: null } } };
+ expect(getPrivacySetting(message)).toBeNull();
+ });
+});
+
+describe('enrichTrackPayload', () => {
+ it('should correctly enrich the payload with custom fields for ecomm events where product array is not supported', () => {
+ const message = {
+ event: 'Product Added',
+ properties: {
+ product_id: 'prd123',
+ sku: 'sku123',
+ brand: 'brand123',
+ property1: 'value1',
+ property2: 'value2',
+ },
+ };
+ const payload = {
+ items: [
+ {
+ item_code: 'prd123',
+ },
+ ],
+ property1: 'value1',
+ property2: 'value2',
+ };
+ const expectedPayload = {
+ items: [
+ {
+ item_code: 'prd123',
+ },
+ ],
+ brand: 'brand123',
+ property1: 'value1',
+ property2: 'value2',
+ };
+
+ const result = enrichTrackPayload(message, payload);
+ expect(result).toEqual(expectedPayload);
+ });
+
+ it('should correctly enrich the payload with custom fields when the for ecomm events with products array support', () => {
+ const message = {
+ event: 'order completed',
+ properties: {
+ order_id: 'ord123',
+ total: 52.0,
+ subtotal: 45.0,
+ revenue: 50.0,
+ products: [{ product_id: 'prd123', sku: 'sku123', brand: 'brand123' }],
+ property1: 'value1',
+ property2: 'value2',
+ },
+ };
+ const payload = {
+ order_id: 'ord123',
+ value: 50.0,
+ items: [{ item_code: 'prd123', brand: 'brand123' }],
+ property1: 'value1',
+ property2: 'value2',
+ };
+ const expectedPayload = {
+ order_id: 'ord123',
+ total: 52.0,
+ subtotal: 45.0,
+ value: 50.0,
+ items: [{ item_code: 'prd123', brand: 'brand123' }],
+ property1: 'value1',
+ property2: 'value2',
+ };
+
+ const result = enrichTrackPayload(message, payload);
+
+ expect(result).toEqual(expectedPayload);
+ });
+
+ it('should return the enriched payload for custom event', () => {
+ const message = {
+ event: 'someEvent',
+ properties: {
+ order_id: 'ord123',
+ property1: 'value1',
+ property2: 'value2',
+ revenue: 10,
+ value: 11,
+ products: [
+ {
+ product_id: 'prd123',
+ test: 'test',
+ },
+ ],
+ },
+ };
+ const payload = {
+ order_id: 'ord123',
+ value: 11,
+ };
+ const expectedPayload = {
+ order_id: 'ord123',
+ property1: 'value1',
+ property2: 'value2',
+ revenue: 10,
+ value: 11,
+ products: [
+ {
+ product_id: 'prd123',
+ test: 'test',
+ },
+ ],
+ };
+
+ const result = enrichTrackPayload(message, payload);
+ expect(result).toEqual(expectedPayload);
+ });
+});
diff --git a/src/cdk/v2/handler.ts b/src/cdk/v2/handler.ts
index 47d6d10179..edd14e7298 100644
--- a/src/cdk/v2/handler.ts
+++ b/src/cdk/v2/handler.ts
@@ -50,16 +50,20 @@ export async function getWorkflowEngine(
const workflowEnginePromiseMap = new Map();
-export function getCachedWorkflowEngine(
+export async function getCachedWorkflowEngine(
destName: string,
feature: string,
bindings: Record = {},
-): WorkflowEngine {
+): Promise {
// Create a new instance of the engine for the destination if needed
// TODO: Use cache to avoid long living engine objects
workflowEnginePromiseMap[destName] = workflowEnginePromiseMap[destName] || new Map();
if (!workflowEnginePromiseMap[destName][feature]) {
- workflowEnginePromiseMap[destName][feature] = getWorkflowEngine(destName, feature, bindings);
+ workflowEnginePromiseMap[destName][feature] = await getWorkflowEngine(
+ destName,
+ feature,
+ bindings,
+ );
}
return workflowEnginePromiseMap[destName][feature];
}
@@ -97,5 +101,8 @@ export function executeStep(
): Promise {
return workflowEngine
.getStepExecutor(stepName)
- .execute(input, Object.assign(workflowEngine.bindings, getEmptyExecutionBindings(), bindings));
+ .execute(
+ input,
+ Object.assign(workflowEngine.getBindings(), getEmptyExecutionBindings(), bindings),
+ );
}
diff --git a/src/util/prometheus.js b/src/util/prometheus.js
index d7ba3b7c61..48868449c3 100644
--- a/src/util/prometheus.js
+++ b/src/util/prometheus.js
@@ -533,7 +533,7 @@ class Prometheus {
name: 'outgoing_request_count',
help: 'Outgoing HTTP requests count',
type: 'counter',
- labelNames: ['feature', 'destType', 'endpointPath', 'success', 'statusCode'],
+ labelNames: ['feature', 'destType', 'endpointPath', 'success', 'statusCode', 'requestMethod' , 'module'],
},
// Gauges
@@ -573,7 +573,7 @@ class Prometheus {
name: 'outgoing_request_latency',
help: 'Outgoing HTTP requests duration in seconds',
type: 'histogram',
- labelNames: ['feature', 'destType', 'endpointPath'],
+ labelNames: ['feature', 'destType', 'endpointPath', 'requestMethod', 'module'],
},
{
name: 'http_request_duration',
diff --git a/src/v0/destinations/active_campaign/transform.js b/src/v0/destinations/active_campaign/transform.js
index 981dbd7520..3978f868b1 100644
--- a/src/v0/destinations/active_campaign/transform.js
+++ b/src/v0/destinations/active_campaign/transform.js
@@ -62,6 +62,8 @@ const syncContact = async (contactPayload, category, destination) => {
destType: 'active_campaign',
feature: 'transformation',
endpointPath: endPoint,
+ requestMethod: 'POST',
+ module: 'router',
});
if (res.success === false) {
errorHandler(res, 'Failed to create new contact');
@@ -129,6 +131,8 @@ const customTagProcessor = async (message, category, destination, contactId) =>
destType: 'active_campaign',
feature: 'transformation',
endpointPath: `/api/3/tags`,
+ requestMethod: 'GET',
+ module: 'router',
});
promises.push(resp);
}
@@ -253,6 +257,8 @@ const customFieldProcessor = async (message, category, destination) => {
destType: 'active_campaign',
feature: 'transformation',
endpointPath: `/api/3/fields`,
+ requestMethod: 'GET',
+ module: 'router',
});
promises.push(resp);
}
@@ -351,6 +357,8 @@ const customListProcessor = async (message, category, destination, contactId) =>
destType: 'active_campaign',
feature: 'transformation',
endpointPath: mergeListWithContactUrl,
+ requestMethod: 'POST',
+ module: 'router',
});
promises.push(res);
}
@@ -409,6 +417,8 @@ const screenRequestHandler = async (message, category, destination) => {
destType: 'active_campaign',
feature: 'transformation',
endpointPath: `/api/3/eventTrackingEvents`,
+ requestMethod: 'GET',
+ module: 'router',
});
if (res.success === false) {
errorHandler(res, 'Failed to retrieve events');
@@ -473,6 +483,8 @@ const trackRequestHandler = async (message, category, destination) => {
destType: 'active_campaign',
feature: 'transformation',
endpointPath: `/api/3/eventTrackingEvents`,
+ requestMethod: 'GET',
+ module: 'router',
});
if (res.success === false) {
diff --git a/src/v0/destinations/af/deleteUsers.js b/src/v0/destinations/af/deleteUsers.js
index bb711292c0..ab515642aa 100644
--- a/src/v0/destinations/af/deleteUsers.js
+++ b/src/v0/destinations/af/deleteUsers.js
@@ -39,6 +39,8 @@ const deleteUser = async (config, endpoint, body, identityType, identityValue) =
destType: 'af',
feature: 'deleteUsers',
endpointPath: `appsflyer.com/api/gdpr/v1/opendsr_requests`,
+ requestMethod: 'POST',
+ module: 'deletion',
},
);
const handledDelResponse = processAxiosResponse(response);
@@ -48,6 +50,7 @@ const deleteUser = async (config, endpoint, body, identityType, identityValue) =
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/af/transform.js b/src/v0/destinations/af/transform.js
index 4d7ed7e635..57629b9483 100644
--- a/src/v0/destinations/af/transform.js
+++ b/src/v0/destinations/af/transform.js
@@ -113,9 +113,15 @@ function getEventValueForUnIdentifiedTrackEvent(message) {
return { eventValue };
}
-function getEventValueMapFromMappingJson(message, mappingJson, isMultiSupport) {
+function getEventValueMapFromMappingJson(message, mappingJson, isMultiSupport, addPropertiesAtRoot) {
let eventValue = {};
- set(eventValue, 'properties', message.properties);
+
+ if (addPropertiesAtRoot) {
+ eventValue = message.properties;
+ } else {
+ set(eventValue, 'properties', message.properties);
+ }
+
const sourceKeys = Object.keys(mappingJson);
sourceKeys.forEach((sourceKey) => {
set(eventValue, mappingJson[sourceKey], get(message, sourceKey));
@@ -160,7 +166,7 @@ function processNonTrackEvents(message, eventName) {
return payload;
}
-function processEventTypeTrack(message) {
+function processEventTypeTrack(message, addPropertiesAtRoot) {
let isMultiSupport = true;
const evType = message.event && message.event.toLowerCase();
let category = ConfigCategory.DEFAULT;
@@ -184,6 +190,7 @@ function processEventTypeTrack(message) {
message,
mappingConfig[category.name],
isMultiSupport,
+ addPropertiesAtRoot,
);
payload.eventName = message.event;
payload.eventCurrency = message?.properties?.currency;
@@ -196,7 +203,7 @@ function processSingleMessage(message, destination) {
let payload;
switch (messageType) {
case EventType.TRACK: {
- payload = processEventTypeTrack(message);
+ payload = processEventTypeTrack(message, destination.Config.addPropertiesAtRoot);
break;
}
case EventType.SCREEN: {
diff --git a/src/v0/destinations/am/deleteUsers.js b/src/v0/destinations/am/deleteUsers.js
index 6de9cf64a1..96c4f7b19c 100644
--- a/src/v0/destinations/am/deleteUsers.js
+++ b/src/v0/destinations/am/deleteUsers.js
@@ -43,6 +43,8 @@ const userDeletionHandler = async (userAttributes, config) => {
destType: 'am',
feature: 'deleteUsers',
endpointPath,
+ requestMethod: 'POST',
+ module: 'deletion',
});
const handledDelResponse = processAxiosResponse(resp);
if (!isHttpStatusSuccess(handledDelResponse.status)) {
@@ -51,6 +53,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/am/transform.js b/src/v0/destinations/am/transform.js
index 911ec51be0..afd72b77e1 100644
--- a/src/v0/destinations/am/transform.js
+++ b/src/v0/destinations/am/transform.js
@@ -22,13 +22,13 @@ const {
getFieldValueFromMessage,
getValueFromMessage,
deleteObjectProperty,
- getErrorRespEvents,
removeUndefinedAndNullValues,
isDefinedAndNotNull,
isAppleFamily,
isDefinedAndNotNullAndNotEmpty,
simpleProcessRouterDest,
isValidInteger,
+ handleRtTfSingleEventError,
} = require('../../util');
const {
BASE_URL,
@@ -40,7 +40,6 @@ const {
AMBatchSizeLimit,
AMBatchEventLimit,
} = require('./config');
-const tags = require('../../util/tags');
const AMUtils = require('./utils');
@@ -615,16 +614,16 @@ const processSingleMessage = (message, destination) => {
case EventType.PAGE:
if (useUserDefinedPageEventName) {
const getMessagePath = userProvidedPageEventString
- .substring(
+ ?.substring(
userProvidedPageEventString.indexOf('{') + 2,
userProvidedPageEventString.indexOf('}'),
)
.trim();
evType =
- userProvidedPageEventString.trim() === ''
+ userProvidedPageEventString?.trim() === ''
? name
: userProvidedPageEventString
- .trim()
+ ?.trim()
.replaceAll(/{{([^{}]+)}}/g, get(message, getMessagePath));
} else {
evType = `Viewed ${name || get(message, CATEGORY_KEY) || ''} Page`;
@@ -702,6 +701,7 @@ const processSingleMessage = (message, destination) => {
logger.debug('could not determine type');
throw new InstrumentationError('message type not supported');
}
+ AMUtils.validateEventType(evType);
return responseBuilderSimple(
groupInfo,
payloadObjectName,
@@ -904,16 +904,10 @@ const batch = (destEvents) => {
// this case shold not happen and should be filtered already
// by the first pass of single event transformation
if (messageEvent && !userId && !deviceId) {
- const errorResponse = getErrorRespEvents(
- metadata,
- 400,
+ const MissingUserIdDeviceIdError = new InstrumentationError(
'Both userId and deviceId cannot be undefined',
- {
- [tags.TAG_NAMES.ERROR_CATEGORY]: tags.ERROR_CATEGORIES.DATA_VALIDATION,
- [tags.TAG_NAMES.ERROR_TYPE]: tags.ERROR_TYPES.INSTRUMENTATION,
- },
);
- respList.push(errorResponse);
+ respList.push(handleRtTfSingleEventError(ev, MissingUserIdDeviceIdError, {}));
return;
}
/* check if not a JSON body or (userId length < 5 && batchEventsWithUserIdLengthLowerThanFive is false) or
diff --git a/src/v0/destinations/am/util.test.js b/src/v0/destinations/am/util.test.js
index faaa9170f0..fa30a74757 100644
--- a/src/v0/destinations/am/util.test.js
+++ b/src/v0/destinations/am/util.test.js
@@ -1,4 +1,4 @@
-const { getUnsetObj } = require('./utils');
+const { getUnsetObj, validateEventType } = require('./utils');
describe('getUnsetObj', () => {
it("should return undefined when 'message.integrations.Amplitude.fieldsToUnset' is not array", () => {
@@ -64,3 +64,36 @@ describe('getUnsetObj', () => {
expect(result).toBeUndefined();
});
});
+
+describe('validateEventType', () => {
+ it('should validate event type when it is valid with only page name given', () => {
+ expect(() => {
+ validateEventType('Home Page');
+ }).not.toThrow();
+ });
+
+ it('should throw an error when event type is null', () => {
+ expect(() => {
+ validateEventType(null);
+ }).toThrow(
+ 'Event type is missing. Please send it under `event.type`. For page/screen events, send it under `event.name`',
+ );
+ });
+
+ it('should throw an error when event type is undefined', () => {
+ expect(() => {
+ validateEventType(undefined);
+ }).toThrow(
+ 'Event type is missing. Please send it under `event.type`. For page/screen events, send it under `event.name`',
+ );
+ });
+
+ // Function receives an empty string as event type
+ it('should throw an error when event type is an empty string', () => {
+ expect(() => {
+ validateEventType('');
+ }).toThrow(
+ 'Event type is missing. Please send it under `event.type`. For page/screen events, send it under `event.name`',
+ );
+ });
+});
diff --git a/src/v0/destinations/am/utils.js b/src/v0/destinations/am/utils.js
index 71fe0ab459..ed1b772fca 100644
--- a/src/v0/destinations/am/utils.js
+++ b/src/v0/destinations/am/utils.js
@@ -10,6 +10,7 @@
// populate these dest keys
const get = require('get-value');
const uaParser = require('@amplitude/ua-parser-js');
+const { InstrumentationError } = require('@rudderstack/integrations-lib');
const logger = require('../../../logger');
const { isDefinedAndNotNull } = require('../../util');
@@ -108,6 +109,19 @@ const getUnsetObj = (message) => {
return unsetObject;
};
+
+/**
+ * Check for evType as in some cases, like when the page name is absent,
+ * either the template depends only on the event.name or there is no template provided by user
+ * @param {*} evType
+ */
+const validateEventType = (evType) => {
+ if (!isDefinedAndNotNull(evType) || (typeof evType === 'string' && evType.length === 0)) {
+ throw new InstrumentationError(
+ 'Event type is missing. Please send it under `event.type`. For page/screen events, send it under `event.name`',
+ );
+ }
+};
module.exports = {
getOSName,
getOSVersion,
@@ -117,4 +131,5 @@ module.exports = {
getBrand,
getEventId,
getUnsetObj,
+ validateEventType,
};
diff --git a/src/v0/destinations/braze/braze.util.test.js b/src/v0/destinations/braze/braze.util.test.js
index 9e82a235f1..7b6a93d359 100644
--- a/src/v0/destinations/braze/braze.util.test.js
+++ b/src/v0/destinations/braze/braze.util.test.js
@@ -305,7 +305,14 @@ describe('dedup utility tests', () => {
},
timeout: 10000,
},
- { destType: 'braze', feature: 'transformation' },
+ {
+ destType: 'braze',
+ feature: 'transformation',
+ endpointPath: '/users/export/ids',
+ feature: 'transformation',
+ module: 'router',
+ requestMethod: 'POST',
+ },
);
});
diff --git a/src/v0/destinations/braze/deleteUsers.js b/src/v0/destinations/braze/deleteUsers.js
index b94d901138..33c0f2ef7f 100644
--- a/src/v0/destinations/braze/deleteUsers.js
+++ b/src/v0/destinations/braze/deleteUsers.js
@@ -22,7 +22,7 @@ const userDeletionHandler = async (userAttributes, config) => {
// Endpoints different for different data centers.
// DOC: https://www.braze.com/docs/user_guide/administrative/access_braze/braze_instances/
let endPoint;
- const endpointPath = '/users/delete'; // TODO: to handle for destinations dynamically by extracting from endpoint
+ const endpointPath = '/users/delete';
const dataCenterArr = dataCenter.trim().split('-');
if (dataCenterArr[0].toLowerCase() === 'eu') {
endPoint = 'https://rest.fra-01.braze.eu/users/delete';
@@ -46,6 +46,8 @@ const userDeletionHandler = async (userAttributes, config) => {
destType: 'braze',
feature: 'deleteUsers',
endpointPath,
+ requestMethod: 'POST',
+ module: 'deletion',
});
const handledDelResponse = processAxiosResponse(resp);
if (!isHttpStatusSuccess(handledDelResponse.status) && handledDelResponse.status !== 404) {
@@ -54,6 +56,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/braze/transform.js b/src/v0/destinations/braze/transform.js
index 6549f5658f..d45640272e 100644
--- a/src/v0/destinations/braze/transform.js
+++ b/src/v0/destinations/braze/transform.js
@@ -223,6 +223,9 @@ async function processIdentify(message, destination) {
{
destType: 'braze',
feature: 'transformation',
+ requestMethod: 'POST',
+ module: 'router',
+ endpointPath: '/users/identify',
},
);
if (!isHttpStatusSuccess(brazeIdentifyResp.status)) {
diff --git a/src/v0/destinations/braze/util.js b/src/v0/destinations/braze/util.js
index 40b9a7eada..5f1f1e6205 100644
--- a/src/v0/destinations/braze/util.js
+++ b/src/v0/destinations/braze/util.js
@@ -163,6 +163,9 @@ const BrazeDedupUtility = {
{
destType: 'braze',
feature: 'transformation',
+ requestMethod: 'POST',
+ module: 'router',
+ endpointPath: '/users/export/ids',
},
);
stats.counter('braze_lookup_failure_count', 1, {
diff --git a/src/v0/destinations/canny/util.js b/src/v0/destinations/canny/util.js
index f514a01e5c..1d03eed4b9 100644
--- a/src/v0/destinations/canny/util.js
+++ b/src/v0/destinations/canny/util.js
@@ -46,6 +46,8 @@ const retrieveUserId = async (apiKey, message) => {
destType: 'canny',
feature: 'transformation',
endpointPath: `/v1/users/retrieve`,
+ requestMethod: 'POST',
+ module: 'processor',
},
);
logger.debug(response);
diff --git a/src/v0/destinations/clevertap/deleteUsers.js b/src/v0/destinations/clevertap/deleteUsers.js
index 3c07a63d93..52119bf0f1 100644
--- a/src/v0/destinations/clevertap/deleteUsers.js
+++ b/src/v0/destinations/clevertap/deleteUsers.js
@@ -53,6 +53,8 @@ const userDeletionHandler = async (userAttributes, config) => {
destType: 'clevertap',
feature: 'deleteUsers',
endpointPath,
+ requestMethod: 'POST',
+ module: 'deletion',
},
);
const handledDelResponse = processAxiosResponse(deletionResponse);
@@ -62,6 +64,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/clevertap/transform.js b/src/v0/destinations/clevertap/transform.js
index efcd101668..b369f507f8 100644
--- a/src/v0/destinations/clevertap/transform.js
+++ b/src/v0/destinations/clevertap/transform.js
@@ -83,16 +83,21 @@ const responseWrapper = (payload, destination) => {
}
*
}
- * This function stringify the payload attributes if it's an array or objects.
+ * This function stringify the payload attributes if it's an array or objects. The keys that are not stringified are present in the `stringifyExcludeList` array.
* @param {*} payload
* @returns
* return the final payload after converting to the relevant data-types.
*/
const convertObjectAndArrayToString = (payload, event) => {
const finalPayload = {};
+ const stringifyExcludeList = ['category-unsubscribe', 'category-resubscribe'];
if (payload) {
Object.keys(payload).forEach((key) => {
- if (payload[key] && (Array.isArray(payload[key]) || typeof payload[key] === 'object')) {
+ if (
+ payload[key] &&
+ (Array.isArray(payload[key]) || typeof payload[key] === 'object') &&
+ !stringifyExcludeList.includes(key)
+ ) {
finalPayload[key] = JSON.stringify(payload[key]);
} else {
finalPayload[key] = payload[key];
diff --git a/src/v0/destinations/clickup/util.js b/src/v0/destinations/clickup/util.js
index 148fe1bd07..74e961906c 100644
--- a/src/v0/destinations/clickup/util.js
+++ b/src/v0/destinations/clickup/util.js
@@ -217,6 +217,9 @@ const retrieveCustomFields = async (listId, apiToken) => {
const customFieldsResponse = await httpGET(endpoint, requestOptions, {
destType: 'clickup',
feature: 'transformation',
+ endpointPath: '/list/listId/field',
+ requestMethod: 'GET',
+ module: 'router',
});
const processedCustomFieldsResponse = processAxiosResponse(customFieldsResponse);
diff --git a/src/v0/destinations/custify/deleteUsers.js b/src/v0/destinations/custify/deleteUsers.js
index 147fcc602c..690768a170 100644
--- a/src/v0/destinations/custify/deleteUsers.js
+++ b/src/v0/destinations/custify/deleteUsers.js
@@ -17,36 +17,44 @@ const userDeletionHandler = async (userAttributes, config) => {
}
const { apiKey } = config;
- const { userId } = userAttributes;
if (!apiKey) {
throw new ConfigurationError('api key for deletion not present', 400);
}
- if (!userId) {
- throw new InstrumentationError('User id for deletion not present', 400);
- }
- const requestUrl = `https://api.custify.com/people?user_id=${userId}`;
- const requestOptions = {
- headers: {
- Authorization: `Bearer ${apiKey}`,
- },
- };
+ await Promise.all(
+ userAttributes.map(async (userAttr) => {
+ const { userId } = userAttr;
+ if (!userId) {
+ throw new InstrumentationError('User id for deletion not present', 400);
+ }
+ // Reference: https://docs.custify.com/#tag/People/paths/~1people/delete
+ const requestUrl = `https://api.custify.com/people?user_id=${userId}`;
+ const requestOptions = {
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ };
- const deletionResponse = await httpDELETE(requestUrl, requestOptions, {
- destType: 'custify',
- feature: 'deleteUsers',
- });
- const processedDeletionRequest = processAxiosResponse(deletionResponse);
- if (processedDeletionRequest.status !== 200 && processedDeletionRequest.status !== 404) {
- throw new NetworkError(
- JSON.stringify(processedDeletionRequest.response) || 'Error while deleting user',
- processedDeletionRequest.status,
- {
- [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(processedDeletionRequest.status),
- },
- deletionResponse,
- );
- }
+ const deletionResponse = await httpDELETE(requestUrl, requestOptions, {
+ destType: 'custify',
+ feature: 'deleteUsers',
+ requestMethod: 'DELETE',
+ endpointPath: '/people',
+ module: 'deletion',
+ });
+ const processedDeletionRequest = processAxiosResponse(deletionResponse);
+ if (processedDeletionRequest.status !== 200 && processedDeletionRequest.status !== 404) {
+ throw new NetworkError(
+ JSON.stringify(processedDeletionRequest.response) || 'Error while deleting user',
+ processedDeletionRequest.status,
+ {
+ [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(processedDeletionRequest.status),
+ },
+ deletionResponse,
+ );
+ }
+ }),
+ );
return { statusCode: 200, status: 'successful' };
};
diff --git a/src/v0/destinations/custify/util.js b/src/v0/destinations/custify/util.js
index 8ecabccd2e..b6f3446503 100644
--- a/src/v0/destinations/custify/util.js
+++ b/src/v0/destinations/custify/util.js
@@ -41,6 +41,8 @@ const createUpdateCompany = async (companyPayload, Config) => {
destType: 'custify',
feature: 'transformation',
endpointPath: `/company`,
+ requestMethod: 'POST',
+ module: 'router',
},
);
const processedCompanyResponse = processAxiosResponse(companyResponse);
diff --git a/src/v0/destinations/delighted/util.js b/src/v0/destinations/delighted/util.js
index 2c92685fd7..c690bf5f5c 100644
--- a/src/v0/destinations/delighted/util.js
+++ b/src/v0/destinations/delighted/util.js
@@ -61,7 +61,13 @@ const userValidity = async (channel, Config, userId) => {
},
params: paramsdata,
},
- { destType: 'delighted', feature: 'transformation' },
+ {
+ 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;
diff --git a/src/v0/destinations/drip/util.js b/src/v0/destinations/drip/util.js
index a502cf0d20..b7015c9351 100644
--- a/src/v0/destinations/drip/util.js
+++ b/src/v0/destinations/drip/util.js
@@ -31,7 +31,13 @@ const userExists = async (Config, id) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'drip', feature: 'transformation' },
+ {
+ destType: 'drip',
+ feature: 'transformation',
+ requestMethod: 'GET',
+ endpointPath: '/subscribers/id',
+ module: 'router',
+ },
);
if (response && response.status) {
return response.status === 200;
@@ -70,7 +76,13 @@ const createUpdateUser = async (finalpayload, Config, basicAuth) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'drip', feature: 'transformation' },
+ {
+ destType: 'drip',
+ feature: 'transformation',
+ requestMethod: 'POST',
+ endpointPath: '/subscribers',
+ module: 'router',
+ },
);
if (response) {
return response.status === 200 || response.status === 201;
diff --git a/src/v0/destinations/engage/deleteUsers.js b/src/v0/destinations/engage/deleteUsers.js
index a3c3055c7d..3616d2408d 100644
--- a/src/v0/destinations/engage/deleteUsers.js
+++ b/src/v0/destinations/engage/deleteUsers.js
@@ -42,6 +42,9 @@ const userDeletionHandler = async (userAttributes, config) => {
{
destType: 'engage',
feature: 'deleteUsers',
+ requestMethod: 'DELETE',
+ endpointPath: '/users/userId',
+ module: 'deletion',
},
);
const handledDelResponse = processAxiosResponse(response);
@@ -51,6 +54,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/freshmarketer/utils.js b/src/v0/destinations/freshmarketer/utils.js
index 5e3ba6e67e..c80711ff8d 100644
--- a/src/v0/destinations/freshmarketer/utils.js
+++ b/src/v0/destinations/freshmarketer/utils.js
@@ -50,6 +50,8 @@ const createUpdateAccount = async (payload, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `/crm/sales/api/sales_accounts/upsert`,
+ requestMethod: 'POST',
+ module: 'router',
});
accountResponse = processAxiosResponse(accountResponse);
if (accountResponse.status !== 200 && accountResponse.status !== 201) {
@@ -95,6 +97,8 @@ const getUserAccountDetails = async (payload, userEmail, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `crm/sales/api/contacts/upsert?include=sales_accounts`,
+ requestMethod: 'POST',
+ module: 'router',
});
userSalesAccountResponse = processAxiosResponse(userSalesAccountResponse);
if (userSalesAccountResponse.status !== 200 && userSalesAccountResponse.status !== 201) {
@@ -145,6 +149,8 @@ const createOrUpdateListDetails = async (listName, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `/crm/sales/api/lists`,
+ requestMethod: 'GET',
+ module: 'router',
});
listResponse = processAxiosResponse(listResponse);
if (listResponse.status !== 200) {
@@ -165,6 +171,8 @@ const createOrUpdateListDetails = async (listName, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `/crm/sales/api/lists`,
+ requestMethod: 'POST',
+ module: 'router',
});
listResponse = processAxiosResponse(listResponse);
if (listResponse.status !== 200) {
@@ -240,6 +248,8 @@ const getContactsDetails = async (userEmail, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `/crm/sales/api/contacts/upsert`,
+ requestMethod: 'POST',
+ module: 'router',
});
userResponse = processAxiosResponse(userResponse);
if (userResponse.status !== 200 && userResponse.status !== 201) {
@@ -314,6 +324,8 @@ const UpdateContactWithLifeCycleStage = async (message, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `/crm/sales/api/selector/lifecycle_stages`,
+ requestMethod: 'GET',
+ module: 'router',
});
lifeCycleStagesResponse = processAxiosResponse(lifeCycleStagesResponse);
if (lifeCycleStagesResponse.status !== 200) {
@@ -400,6 +412,8 @@ const UpdateContactWithSalesActivity = async (payload, message, Config) => {
destType: 'freshmarketer',
feature: 'transformation',
endpointPath: `/crm/sales/api/selector/sales_activity_types`,
+ requestMethod: 'GET',
+ module: 'router',
});
salesActivityResponse = processAxiosResponse(salesActivityResponse);
if (salesActivityResponse.status !== 200) {
diff --git a/src/v0/destinations/freshsales/utils.js b/src/v0/destinations/freshsales/utils.js
index 5008fedc2d..977bde0abb 100644
--- a/src/v0/destinations/freshsales/utils.js
+++ b/src/v0/destinations/freshsales/utils.js
@@ -48,6 +48,8 @@ const createUpdateAccount = async (payload, Config) => {
destType: 'freshsales',
feature: 'transformation',
endpointPath: `/crm/sales/api/sales_accounts/upsert`,
+ requestMethod: 'POST',
+ module: 'router',
});
accountResponse = processAxiosResponse(accountResponse);
if (accountResponse.status !== 200 && accountResponse.status !== 201) {
@@ -92,6 +94,8 @@ const getUserAccountDetails = async (payload, userEmail, Config) => {
destType: 'freshsales',
feature: 'transformation',
endpointPath: `/crm/sales/api/contacts/upsert?include=sales_accounts`,
+ requestMethod: 'POST',
+ module: 'router',
});
userSalesAccountResponse = processAxiosResponse(userSalesAccountResponse);
if (userSalesAccountResponse.status !== 200 && userSalesAccountResponse.status !== 201) {
@@ -148,6 +152,8 @@ const getContactsDetails = async (userEmail, Config) => {
destType: 'freshsales',
feature: 'transformation',
endpointPath: `/crm/sales/api/contacts/upsert`,
+ requestMethod: 'POST',
+ module: 'router',
});
userResponse = processAxiosResponse(userResponse);
if (userResponse.status !== 200 && userResponse.status !== 201) {
@@ -239,6 +245,8 @@ const UpdateContactWithSalesActivity = async (payload, message, Config) => {
destType: 'freshsales',
feature: 'transformation',
endpointPath: `/crm/sales/api/sales_activity_types`,
+ requestMethod: 'GET',
+ module: 'router',
});
salesActivityResponse = processAxiosResponse(salesActivityResponse);
if (salesActivityResponse.status !== 200) {
@@ -319,6 +327,8 @@ const UpdateContactWithLifeCycleStage = async (message, Config) => {
destType: 'freshsales',
feature: 'transformation',
endpointPath: `/crm/sales/api/lifecycle_stages`,
+ requestMethod: 'GET',
+ module: 'router',
});
lifeCycleStagesResponse = processAxiosResponse(lifeCycleStagesResponse);
if (lifeCycleStagesResponse.status !== 200) {
diff --git a/src/v0/destinations/ga/deleteUsers.js b/src/v0/destinations/ga/deleteUsers.js
index 06e674048a..524e2c14b4 100644
--- a/src/v0/destinations/ga/deleteUsers.js
+++ b/src/v0/destinations/ga/deleteUsers.js
@@ -81,6 +81,8 @@ const userDeletionHandler = async (userAttributes, config, rudderDestInfo) => {
destType: 'ga',
feature: 'deleteUsers',
endpointPath: '/userDeletion/userDeletionRequests:upsert',
+ requestMethod: 'POST',
+ module: 'deletion',
},
);
// process the response to know about refreshing scenario
diff --git a/src/v0/destinations/ga4/networkHandler.js b/src/v0/destinations/ga4/networkHandler.js
index e4ca1effa8..da8ae2ea30 100644
--- a/src/v0/destinations/ga4/networkHandler.js
+++ b/src/v0/destinations/ga4/networkHandler.js
@@ -31,9 +31,9 @@ const responseHandler = (responseParams) => {
const { description, validationCode, fieldPath } = response.validationMessages[0];
throw new NetworkError(
`Validation Server Response Handler:: Validation Error for ${destType} of field path :${fieldPath} | ${validationCode}-${description}`,
- status,
+ 400,
{
- [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status),
+ [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(400),
},
response?.validationMessages[0]?.description,
);
diff --git a/src/v0/destinations/gainsight/util.js b/src/v0/destinations/gainsight/util.js
index 39e666c1a5..4c7fd58193 100644
--- a/src/v0/destinations/gainsight/util.js
+++ b/src/v0/destinations/gainsight/util.js
@@ -22,7 +22,13 @@ const searchGroup = async (groupName, Config) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'gainsight', feature: 'transformation' },
+ {
+ destType: 'gainsight',
+ feature: 'transformation',
+ requestMethod: 'POST',
+ endpointPath: '/data/objects/query/Company',
+ module: 'router',
+ },
);
} catch (error) {
let errMessage = '';
@@ -56,7 +62,13 @@ const createGroup = async (payload, Config) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'gainsight', feature: 'transformation' },
+ {
+ destType: 'gainsight',
+ feature: 'transformation',
+ requestMethod: 'POST',
+ endpointPath: '/data/objects/Company',
+ module: 'router',
+ },
);
} catch (error) {
let errMessage = '';
@@ -93,7 +105,13 @@ const updateGroup = async (payload, Config) => {
keys: 'Name',
},
},
- { destType: 'gainsight', feature: 'transformation' },
+ {
+ destType: 'gainsight',
+ feature: 'transformation',
+ requestMethod: 'PUT',
+ endpointPath: '/data/objects/Company',
+ module: 'router',
+ },
);
} catch (error) {
let errMessage = '';
diff --git a/src/v0/destinations/gainsight_px/util.js b/src/v0/destinations/gainsight_px/util.js
index 5109286b3f..e03fbbf148 100644
--- a/src/v0/destinations/gainsight_px/util.js
+++ b/src/v0/destinations/gainsight_px/util.js
@@ -56,7 +56,13 @@ const objectExists = async (id, Config, objectType) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'gainsight_px', feature: 'transformation' },
+ {
+ destType: 'gainsight_px',
+ feature: 'transformation',
+ requestMethod: 'GET',
+ endpointPath: '/accounts/accountId',
+ module: 'router',
+ },
);
if (response && response.status === 200) {
return { success: true, err: null };
@@ -88,7 +94,13 @@ const createAccount = async (payload, Config) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'gainsight_px', feature: 'transformation' },
+ {
+ destType: 'gainsight_px',
+ feature: 'transformation',
+ requestMethod: 'POST',
+ endpointPath: '/accounts',
+ module: 'router',
+ },
);
if (response && response.status === 201) {
return { success: true, err: null };
@@ -121,7 +133,13 @@ const updateAccount = async (accountId, payload, Config) => {
'Content-Type': JSON_MIME_TYPE,
},
},
- { destType: 'gainsight_px', feature: 'transformation' },
+ {
+ destType: 'gainsight_px',
+ feature: 'transformation',
+ requestMethod: 'PUT',
+ endpointPath: '/accounts/accountId',
+ module: 'router',
+ },
);
if (response && response.status === 204) {
return { success: true, err: null };
diff --git a/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js b/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js
index b4590fb71c..3ea985e773 100644
--- a/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js
+++ b/src/v0/destinations/google_adwords_enhanced_conversions/networkHandler.js
@@ -45,6 +45,8 @@ const getConversionActionId = async (method, headers, params) => {
destType: 'google_adwords_enhanced_conversions',
feature: 'proxy',
endpointPath: `/googleAds:searchStream`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
},
);
if (!isHttpStatusSuccess(gaecConversionActionIdResponse.status)) {
@@ -98,6 +100,8 @@ const ProxyRequest = async (request) => {
destType: 'google_adwords_enhanced_conversions',
feature: 'proxy',
endpointPath: `/googleAds:uploadOfflineUserData`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
});
return response;
};
diff --git a/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js b/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js
index 318b7802df..5541fd6e1e 100644
--- a/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js
+++ b/src/v0/destinations/google_adwords_offline_conversions/networkHandler.js
@@ -34,6 +34,8 @@ const createJob = async (endpoint, headers, payload) => {
destType: 'google_adwords_offline_conversions',
feature: 'proxy',
endpointPath: `/create`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
},
);
createJobResponse = processAxiosResponse(createJobResponse);
@@ -59,6 +61,8 @@ const addConversionToJob = async (endpoint, headers, jobId, payload) => {
destType: 'google_adwords_offline_conversions',
feature: 'proxy',
endpointPath: `/addOperations`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
},
);
addConversionToJobResponse = processAxiosResponse(addConversionToJobResponse);
@@ -83,6 +87,8 @@ const runTheJob = async (endpoint, headers, payload, jobId) => {
destType: 'google_adwords_offline_conversions',
feature: 'proxy',
endpointPath: `/run`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
},
);
return executeJobResponse;
@@ -110,6 +116,8 @@ const getConversionCustomVariable = async (headers, params) => {
destType: 'google_adwords_offline_conversions',
feature: 'proxy',
endpointPath: `/searchStream`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
});
searchStreamResponse = processAxiosResponse(searchStreamResponse);
if (!isHttpStatusSuccess(searchStreamResponse.status)) {
@@ -247,6 +255,8 @@ const ProxyRequest = async (request) => {
feature: 'proxy',
destType: 'gogole_adwords_offline_conversions',
endpointPath: `/proxy`,
+ requestMethod: 'POST',
+ module: 'dataDelivery',
});
return response;
};
diff --git a/src/v0/destinations/google_adwords_offline_conversions/transform.js b/src/v0/destinations/google_adwords_offline_conversions/transform.js
index 397895c603..46cde72771 100644
--- a/src/v0/destinations/google_adwords_offline_conversions/transform.js
+++ b/src/v0/destinations/google_adwords_offline_conversions/transform.js
@@ -10,6 +10,7 @@ const {
defaultBatchRequestConfig,
getSuccessRespEvents,
checkInvalidRtTfEvents,
+ combineBatchRequestsWithSameJobIds,
} = require('../../util');
const {
CALL_CONVERSION,
@@ -229,7 +230,7 @@ const processRouterDest = async (inputs, reqMetadata) => {
.concat(storeSalesEventsBatchedResponseList)
.concat(clickCallEvents)
.concat(errorRespList);
- return batchedResponseList;
+ return combineBatchRequestsWithSameJobIds(batchedResponseList);
};
module.exports = { process, processRouterDest };
diff --git a/src/v0/destinations/google_adwords_offline_conversions/utils.js b/src/v0/destinations/google_adwords_offline_conversions/utils.js
index 599a163c54..19989d0eaa 100644
--- a/src/v0/destinations/google_adwords_offline_conversions/utils.js
+++ b/src/v0/destinations/google_adwords_offline_conversions/utils.js
@@ -64,6 +64,8 @@ const getConversionActionId = async (headers, params) => {
destType: 'google_adwords_offline_conversions',
feature: 'transformation',
endpointPath: `/googleAds:searchStream`,
+ requestMethod: 'POST',
+ module: 'dataDelivery'
});
searchStreamResponse = processAxiosResponse(searchStreamResponse);
if (!isHttpStatusSuccess(searchStreamResponse.status)) {
diff --git a/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js b/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js
index dbd055f1a1..3045c1713f 100644
--- a/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js
+++ b/src/v0/destinations/google_adwords_remarketing_lists/networkHandler.js
@@ -41,6 +41,8 @@ const createJob = async (endpoint, headers, method, params) => {
destType: 'google_adwords_remarketing_lists',
feature: 'proxy',
endpointPath: '/customers/create',
+ requestMethod: 'POST',
+ module: 'dataDelivery',
});
return response;
};
@@ -65,6 +67,8 @@ const addUserToJob = async (endpoint, headers, method, jobId, body) => {
destType: 'google_adwords_remarketing_lists',
feature: 'proxy',
endpointPath: '/addOperations',
+ requestMethod: 'POST',
+ module: 'dataDelivery',
});
return response;
};
@@ -87,6 +91,8 @@ const runTheJob = async (endpoint, headers, method, jobId) => {
destType: 'google_adwords_remarketing_lists',
feature: 'proxy',
endpointPath: '/run',
+ requestMethod: 'POST',
+ module: 'dataDelivery',
});
return response;
};
diff --git a/src/v0/destinations/hs/util.js b/src/v0/destinations/hs/util.js
index e905ee63c4..32ee923f5f 100644
--- a/src/v0/destinations/hs/util.js
+++ b/src/v0/destinations/hs/util.js
@@ -104,6 +104,8 @@ const getProperties = async (destination) => {
destType: 'hs',
feature: 'transformation',
endpointPath: `/properties/v1/contacts/properties`,
+ requestMethod: 'GET',
+ module: 'router',
});
hubspotPropertyMapResponse = processAxiosResponse(hubspotPropertyMapResponse);
} else {
@@ -116,6 +118,8 @@ const getProperties = async (destination) => {
destType: 'hs',
feature: 'transformation',
endpointPath: `/properties/v1/contacts/properties?hapikey`,
+ requestMethod: 'GET',
+ module: 'router',
},
);
hubspotPropertyMapResponse = processAxiosResponse(hubspotPropertyMapResponse);
@@ -365,6 +369,8 @@ const searchContacts = async (message, destination) => {
destType: 'hs',
feature: 'transformation',
endpointPath,
+ requestMethod: 'POST',
+ module: 'router',
},
);
searchContactsResponse = processAxiosResponse(searchContactsResponse);
@@ -375,6 +381,8 @@ const searchContacts = async (message, destination) => {
destType: 'hs',
feature: 'transformation',
endpointPath,
+ requestMethod: 'POST',
+ module: 'router',
});
searchContactsResponse = processAxiosResponse(searchContactsResponse);
}
@@ -539,6 +547,8 @@ const performHubSpotSearch = async (
destType: 'hs',
feature: 'transformation',
endpointPath,
+ requestMethod: 'POST',
+ module: 'router',
});
const processedResponse = processAxiosResponse(searchResponse);
diff --git a/src/v0/destinations/intercom/deleteUsers.js b/src/v0/destinations/intercom/deleteUsers.js
index b91f520ade..2c35f29e53 100644
--- a/src/v0/destinations/intercom/deleteUsers.js
+++ b/src/v0/destinations/intercom/deleteUsers.js
@@ -39,6 +39,8 @@ const userDeletionHandler = async (userAttributes, config) => {
destType: 'intercom',
feature: 'deleteUsers',
endpointPath: '/user_delete_requests',
+ requestMethod: 'POST',
+ module: 'deletion',
});
const handledDelResponse = processAxiosResponse(resp);
if (!isHttpStatusSuccess(handledDelResponse.status) && handledDelResponse.status !== 404) {
@@ -47,6 +49,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/iterable/deleteUsers.js b/src/v0/destinations/iterable/deleteUsers.js
index a179a8930f..015a9de9a0 100644
--- a/src/v0/destinations/iterable/deleteUsers.js
+++ b/src/v0/destinations/iterable/deleteUsers.js
@@ -36,6 +36,9 @@ const userDeletionHandler = async (userAttributes, config) => {
const resp = await httpDELETE(url, requestOptions, {
destType: 'iterable',
feature: 'deleteUsers',
+ endpointPath: '/users/byUserId/uId',
+ requestMethod: 'DELETE',
+ module: 'deletion',
});
const handledDelResponse = processAxiosResponse(resp);
if (!isHttpStatusSuccess(handledDelResponse.status) && handledDelResponse.status !== 404) {
@@ -46,6 +49,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/klaviyo/util.js b/src/v0/destinations/klaviyo/util.js
index 60b334f3a2..df2dbb4712 100644
--- a/src/v0/destinations/klaviyo/util.js
+++ b/src/v0/destinations/klaviyo/util.js
@@ -45,6 +45,8 @@ const getIdFromNewOrExistingProfile = async (endpoint, payload, requestOptions)
destType: 'klaviyo',
feature: 'transformation',
endpointPath,
+ requestMethod: 'POST',
+ module: 'router',
},
);
diff --git a/src/v0/destinations/kustomer/util.js b/src/v0/destinations/kustomer/util.js
index 571a03f139..530983bb26 100644
--- a/src/v0/destinations/kustomer/util.js
+++ b/src/v0/destinations/kustomer/util.js
@@ -139,7 +139,13 @@ const fetchKustomer = async (url, destination) => {
Authorization: `Bearer ${destination.Config.apiKey}`,
},
},
- { destType: 'kustomer', feature: 'transformation' },
+ {
+ destType: 'kustomer',
+ feature: 'transformation',
+ endpointPath: '/customers/email',
+ requestMethod: 'GET',
+ module: 'processor',
+ },
);
} catch (err) {
if (err.response) {
diff --git a/src/v0/destinations/mailchimp/utils.js b/src/v0/destinations/mailchimp/utils.js
index e1e2e9883b..1f4fc03ee5 100644
--- a/src/v0/destinations/mailchimp/utils.js
+++ b/src/v0/destinations/mailchimp/utils.js
@@ -163,7 +163,13 @@ const checkIfMailExists = async (apiKey, datacenterId, audienceId, email) => {
Authorization: `Basic ${basicAuth}`,
},
},
- { destType: 'mailchimp', feature: 'transformation' },
+ {
+ destType: 'mailchimp',
+ feature: 'transformation',
+ endpointPath: '/lists/audienceId/members/email',
+ requestMethod: 'GET',
+ module: 'router',
+ },
);
if (response?.data?.contact_id) {
userStatus.exists = true;
@@ -194,7 +200,13 @@ const checkIfDoubleOptIn = async (apiKey, datacenterId, audienceId) => {
Authorization: `Basic ${basicAuth}`,
},
},
- { destType: 'mailchimp', feature: 'transformation' },
+ {
+ destType: 'mailchimp',
+ feature: 'transformation',
+ endpointPath: '/lists/audienceId',
+ requestMethod: 'GET',
+ module: 'router',
+ },
);
} catch (error) {
const status = error.status || 400;
diff --git a/src/v0/destinations/marketo/util.js b/src/v0/destinations/marketo/util.js
index 54ff70708a..b3a24fb411 100644
--- a/src/v0/destinations/marketo/util.js
+++ b/src/v0/destinations/marketo/util.js
@@ -248,6 +248,8 @@ const sendGetRequest = async (url, options) => {
destType: 'marketo',
feature: 'transformation',
endpointPath: `/v1/leads`,
+ requestMethod: 'GET',
+ module: 'router',
});
const processedResponse = processAxiosResponse(clientResponse);
return processedResponse;
@@ -264,6 +266,8 @@ const sendPostRequest = async (url, data, options) => {
destType: 'marketo',
feature: 'transformation',
endpointPath: `/v1/leads`,
+ requestMethod: 'POST',
+ module: 'router',
});
const processedResponse = processAxiosResponse(clientResponse);
return processedResponse;
diff --git a/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js b/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js
index e6f5662000..db3b13eeb8 100644
--- a/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js
+++ b/src/v0/destinations/marketo_bulk_upload/fetchJobStatus.js
@@ -33,6 +33,9 @@ const getJobsStatus = async (event, type, accessToken) => {
const { processedResponse: resp } = await handleHttpRequest('get', url, requestOptions, {
destType: 'marketo_bulk_upload',
feature: 'transformation',
+ endpointPath: '/leads/batch/',
+ requestMethod: 'GET',
+ module: 'router',
});
const endTime = Date.now();
const requestTime = endTime - startTime;
diff --git a/src/v0/destinations/marketo_bulk_upload/fileUpload.js b/src/v0/destinations/marketo_bulk_upload/fileUpload.js
index 9c42fdc98d..b49a265fd5 100644
--- a/src/v0/destinations/marketo_bulk_upload/fileUpload.js
+++ b/src/v0/destinations/marketo_bulk_upload/fileUpload.js
@@ -198,6 +198,9 @@ const getImportID = async (input, config, accessToken, csvHeader) => {
{
destType: 'marketo_bulk_upload',
feature: 'transformation',
+ endpointPath: '/leads.json',
+ requestMethod: 'POST',
+ module: 'router',
},
);
const endTime = Date.now();
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 875b0d8280..aa4b3aacc4 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
@@ -296,6 +296,10 @@ describe('getAccessToken', () => {
expect(handleHttpRequest).toHaveBeenCalledWith('get', url, {
destType: 'marketo_bulk_upload',
feature: 'transformation',
+ endpointPath: '/identity/oauth/token',
+ feature: 'transformation',
+ module: 'router',
+ requestMethod: 'GET',
});
});
diff --git a/src/v0/destinations/marketo_bulk_upload/poll.js b/src/v0/destinations/marketo_bulk_upload/poll.js
index db7a634774..f53347d6e5 100644
--- a/src/v0/destinations/marketo_bulk_upload/poll.js
+++ b/src/v0/destinations/marketo_bulk_upload/poll.js
@@ -26,6 +26,9 @@ const getPollStatus = async (event) => {
{
destType: 'marketo_bulk_upload',
feature: 'transformation',
+ endpointPath: '/leads/batch/importId.json',
+ requestMethod: 'GET',
+ module: 'router',
},
);
if (!isHttpStatusSuccess(pollStatus.status)) {
diff --git a/src/v0/destinations/marketo_bulk_upload/util.js b/src/v0/destinations/marketo_bulk_upload/util.js
index fac04af431..4c99ba7483 100644
--- a/src/v0/destinations/marketo_bulk_upload/util.js
+++ b/src/v0/destinations/marketo_bulk_upload/util.js
@@ -128,6 +128,9 @@ const getAccessToken = async (config) => {
const { processedResponse: accessTokenResponse } = await handleHttpRequest('get', url, {
destType: 'marketo_bulk_upload',
feature: 'transformation',
+ endpointPath: '/identity/oauth/token',
+ requestMethod: 'GET',
+ module: 'router',
});
// sample response : {response: '[ENOTFOUND] :: DNS lookup failed', status: 400}
@@ -352,6 +355,9 @@ const getFieldSchemaMap = async (accessToken, munchkinId) => {
{
destType: 'marketo_bulk_upload',
feature: 'transformation',
+ endpointPath: '/leads/describe2.json',
+ requestMethod: 'GET',
+ module: 'router',
},
);
diff --git a/src/v0/destinations/mautic/utils.js b/src/v0/destinations/mautic/utils.js
index d8ad8dbffc..7a1827e769 100644
--- a/src/v0/destinations/mautic/utils.js
+++ b/src/v0/destinations/mautic/utils.js
@@ -183,6 +183,9 @@ const searchContactIds = async (message, Config, baseUrl) => {
{
destType: 'mautic',
feature: 'transformation',
+ endpointPath: '/contacts',
+ requestMethod: 'GET',
+ module: 'router',
},
);
searchContactsResponse = processAxiosResponse(searchContactsResponse);
diff --git a/src/v0/destinations/monday/util.js b/src/v0/destinations/monday/util.js
index 736f0133fd..872fad42a7 100644
--- a/src/v0/destinations/monday/util.js
+++ b/src/v0/destinations/monday/util.js
@@ -195,6 +195,8 @@ const getBoardDetails = async (url, boardID, apiToken) => {
destType: 'monday',
feature: 'transformation',
endpointPath: '/v2',
+ requestMethod: 'POST',
+ module: 'router',
},
);
const boardDetailsResponse = processAxiosResponse(clientResponse);
diff --git a/src/v0/destinations/mp/deleteUsers.js b/src/v0/destinations/mp/deleteUsers.js
index f01475ef2b..e1240c609d 100644
--- a/src/v0/destinations/mp/deleteUsers.js
+++ b/src/v0/destinations/mp/deleteUsers.js
@@ -49,6 +49,8 @@ const deleteProfile = async (userAttributes, config) => {
destType: 'mp',
feature: 'deleteUsers',
endpointPath,
+ requestMethod: 'POST',
+ module: 'deletion',
},
);
if (!isHttpStatusSuccess(handledDelResponse.status)) {
@@ -57,6 +59,7 @@ const deleteProfile = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
@@ -104,6 +107,8 @@ const createDeletionTask = async (userAttributes, config) => {
destType: 'mp',
feature: 'deleteUsers',
endpointPath,
+ requestMethod: 'POST',
+ module: 'deletion',
},
);
if (!isHttpStatusSuccess(handledDelResponse.status)) {
diff --git a/src/v0/destinations/mp/transform.js b/src/v0/destinations/mp/transform.js
index 41a814683d..24890c0eb1 100644
--- a/src/v0/destinations/mp/transform.js
+++ b/src/v0/destinations/mp/transform.js
@@ -18,6 +18,7 @@ const {
handleRtTfSingleEventError,
groupEventsByType,
parseConfigArray,
+ combineBatchRequestsWithSameJobIds,
} = require('../../util');
const {
ConfigCategory,
@@ -33,7 +34,6 @@ const {
createIdentifyResponse,
isImportAuthCredentialsAvailable,
buildUtmParams,
- combineBatchRequestsWithSameJobIds,
groupEventsByEndpoint,
batchEvents,
trimTraits,
diff --git a/src/v0/destinations/mp/util.js b/src/v0/destinations/mp/util.js
index c01d2308b7..8e943f41dd 100644
--- a/src/v0/destinations/mp/util.js
+++ b/src/v0/destinations/mp/util.js
@@ -139,44 +139,6 @@ const isImportAuthCredentialsAvailable = (destination) =>
destination.Config.serviceAccountUserName &&
destination.Config.projectId);
-/**
- * Finds an existing batch based on metadata JobIds from the provided batch and metadataMap.
- * @param {*} batch
- * @param {*} metadataMap The map containing metadata items indexed by JobIds.
- * @returns
- */
-const findExistingBatch = (batch, metadataMap) => {
- let existingBatch = null;
-
- // eslint-disable-next-line no-restricted-syntax
- for (const metadataItem of batch.metadata) {
- if (metadataMap.has(metadataItem.jobId)) {
- existingBatch = metadataMap.get(metadataItem.jobId);
- break;
- }
- }
-
- return existingBatch;
-};
-
-/**
- * Removes duplicate metadata within each merged batch object.
- * @param {*} mergedBatches An array of merged batch objects.
- */
-const removeDuplicateMetadata = (mergedBatches) => {
- mergedBatches.forEach((batch) => {
- const metadataSet = new Set();
- // eslint-disable-next-line no-param-reassign
- batch.metadata = batch.metadata.filter((metadataItem) => {
- if (!metadataSet.has(metadataItem.jobId)) {
- metadataSet.add(metadataItem.jobId);
- return true;
- }
- return false;
- });
- });
-};
-
/**
* Builds UTM parameters from a campaign object.
*
@@ -273,58 +235,6 @@ const batchEvents = (successRespList, maxBatchSize, reqMetadata) => {
});
};
-/**
- * Combines batched requests with the same JobIds.
- * @param {*} inputBatches The array of batched request objects.
- * @returns The combined batched requests with merged JobIds.
- *
- */
-const combineBatchRequestsWithSameJobIds = (inputBatches) => {
- const combineBatches = (batches) => {
- const clonedBatches = [...batches];
- const mergedBatches = [];
- const metadataMap = new Map();
-
- clonedBatches.forEach((batch) => {
- const existingBatch = findExistingBatch(batch, metadataMap);
-
- if (existingBatch) {
- // Merge batchedRequests arrays
- existingBatch.batchedRequest = [
- ...(Array.isArray(existingBatch.batchedRequest)
- ? existingBatch.batchedRequest
- : [existingBatch.batchedRequest]),
- ...(Array.isArray(batch.batchedRequest) ? batch.batchedRequest : [batch.batchedRequest]),
- ];
-
- // Merge metadata
- batch.metadata.forEach((metadataItem) => {
- if (!metadataMap.has(metadataItem.jobId)) {
- metadataMap.set(metadataItem.jobId, existingBatch);
- }
- existingBatch.metadata.push(metadataItem);
- });
- } else {
- mergedBatches.push(batch);
- batch.metadata.forEach((metadataItem) => {
- metadataMap.set(metadataItem.jobId, batch);
- });
- }
- });
-
- // Remove duplicate metadata within each merged object
- removeDuplicateMetadata(mergedBatches);
-
- return mergedBatches;
- };
- // We need to run this twice because in first pass some batches might not get merged
- // and in second pass they might get merged
- // Example: [[{jobID:1}, {jobID:2}], [{jobID:3}], [{jobID:1}, {jobID:3}]]
- // 1st pass: [[{jobID:1}, {jobID:2}, {jobID:3}], [{jobID:3}]]
- // 2nd pass: [[{jobID:1}, {jobID:2}, {jobID:3}]]
- return combineBatches(combineBatches(inputBatches));
-};
-
/**
* Trims the traits and contextTraits objects based on the setOnceProperties array and returns an object containing the modified traits, contextTraits, and setOnce properties.
*
@@ -398,6 +308,5 @@ module.exports = {
groupEventsByEndpoint,
generateBatchedPayloadForArray,
batchEvents,
- combineBatchRequestsWithSameJobIds,
trimTraits,
};
diff --git a/src/v0/destinations/mp/util.test.js b/src/v0/destinations/mp/util.test.js
index ebf140fadd..866119a336 100644
--- a/src/v0/destinations/mp/util.test.js
+++ b/src/v0/destinations/mp/util.test.js
@@ -1,5 +1,4 @@
const {
- combineBatchRequestsWithSameJobIds,
groupEventsByEndpoint,
batchEvents,
generateBatchedPayloadForArray,
@@ -263,235 +262,6 @@ describe('Mixpanel utils test', () => {
});
});
- describe('Unit test cases for combineBatchRequestsWithSameJobIds', () => {
- it('Combine batch request with same jobIds', async () => {
- const input = [
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/track/',
- },
- metadata: [
- {
- jobId: 1,
- },
- {
- jobId: 4,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/import/',
- },
- metadata: [
- {
- jobId: 3,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/track/',
- },
- metadata: [
- {
- jobId: 5,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/engage/',
- },
- metadata: [
- {
- jobId: 1,
- },
- {
- jobId: 3,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/import/',
- },
- metadata: [
- {
- jobId: 6,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- ];
-
- const expectedOutput = [
- {
- batchedRequest: [
- {
- endpoint: 'https://api.mixpanel.com/track/',
- },
- {
- endpoint: 'https://api.mixpanel.com/engage/',
- },
- {
- endpoint: 'https://api.mixpanel.com/import/',
- },
- ],
- metadata: [
- {
- jobId: 1,
- },
- {
- jobId: 4,
- },
- {
- jobId: 3,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/track/',
- },
- metadata: [
- {
- jobId: 5,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/import/',
- },
- metadata: [
- {
- jobId: 6,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- ];
- expect(combineBatchRequestsWithSameJobIds(input)).toEqual(expectedOutput);
- });
-
- it('Each batchRequest contains unique jobIds (no event multiplexing)', async () => {
- const input = [
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/track/',
- },
- metadata: [
- {
- jobId: 1,
- },
- {
- jobId: 4,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/engage/',
- },
- metadata: [
- {
- jobId: 2,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/engage/',
- },
- metadata: [
- {
- jobId: 5,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- ];
-
- const expectedOutput = [
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/track/',
- },
-
- metadata: [
- {
- jobId: 1,
- },
- {
- jobId: 4,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/engage/',
- },
- metadata: [
- {
- jobId: 2,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- {
- batchedRequest: {
- endpoint: 'https://api.mixpanel.com/engage/',
- },
- metadata: [
- {
- jobId: 5,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: destinationMock,
- },
- ];
- expect(combineBatchRequestsWithSameJobIds(input)).toEqual(expectedOutput);
- });
- });
-
describe('Unit test cases for generateBatchedPayloadForArray', () => {
it('should generate a batched payload with GZIP payload for /import endpoint when given an array of events', () => {
const events = [
diff --git a/src/v0/destinations/profitwell/utils.js b/src/v0/destinations/profitwell/utils.js
index acc4db2035..1b23561721 100644
--- a/src/v0/destinations/profitwell/utils.js
+++ b/src/v0/destinations/profitwell/utils.js
@@ -188,6 +188,9 @@ const getSubscriptionHistory = async (endpoint, options) => {
const res = await httpGET(endpoint, requestOptions, {
destType: 'profitwell',
feature: 'transformation',
+ endpointPath: '/users/userId',
+ requestMethod: 'GET',
+ module: 'router',
});
return res;
};
diff --git a/src/v0/destinations/rakuten/networkHandler.js b/src/v0/destinations/rakuten/networkHandler.js
index 6c89d83947..4c97a23e51 100644
--- a/src/v0/destinations/rakuten/networkHandler.js
+++ b/src/v0/destinations/rakuten/networkHandler.js
@@ -18,7 +18,13 @@ const proxyRequest = async (request, destType) => {
headers,
method,
};
- const response = await httpSend(requestOptions, { feature: 'proxy', destType });
+ const response = await httpSend(requestOptions, {
+ feature: 'proxy',
+ destType,
+ endpointPath: '/ep',
+ requestMethod: 'GET',
+ module: 'dataDelivery',
+ });
return response;
};
const extractContent = (xmlPayload, tagName) => {
diff --git a/src/v0/destinations/salesforce/transform.js b/src/v0/destinations/salesforce/transform.js
index 5ada9dfaa0..e791bffd46 100644
--- a/src/v0/destinations/salesforce/transform.js
+++ b/src/v0/destinations/salesforce/transform.js
@@ -120,6 +120,9 @@ async function getSaleforceIdForRecord(
{
destType: 'salesforce',
feature: 'transformation',
+ endpointPath: '/parameterizedSearch',
+ requestMethod: 'GET',
+ module: 'router',
},
);
if (!isHttpStatusSuccess(processedsfSearchResponse.status)) {
@@ -233,6 +236,9 @@ async function getSalesforceIdFromPayload(
{
destType: 'salesforce',
feature: 'transformation',
+ endpointPath: '/parameterizedSearch',
+ requestMethod: 'GET',
+ module: 'router',
},
);
diff --git a/src/v0/destinations/salesforce/utils.js b/src/v0/destinations/salesforce/utils.js
index 96735ecc17..85061ce2b2 100644
--- a/src/v0/destinations/salesforce/utils.js
+++ b/src/v0/destinations/salesforce/utils.js
@@ -133,6 +133,9 @@ const getAccessToken = async (destination) => {
{
destType: 'salesforce',
feature: 'transformation',
+ endpointPath: '/services/oauth2/token',
+ requestMethod: 'POST',
+ module: 'router',
},
);
// If the request fails, throwing error.
diff --git a/src/v0/destinations/sendgrid/deleteUsers.js b/src/v0/destinations/sendgrid/deleteUsers.js
index 8410f41296..ccd277d90d 100644
--- a/src/v0/destinations/sendgrid/deleteUsers.js
+++ b/src/v0/destinations/sendgrid/deleteUsers.js
@@ -85,6 +85,9 @@ const userDeletionHandler = async (userAttributes, config) => {
const deletionResponse = await httpDELETE(endpoint, requestOptions, {
destType: 'sendgrid',
feature: 'deleteUsers',
+ endpointPath: '/marketing/contacts',
+ requestMethod: 'DELETE',
+ module: 'deletion',
});
const handledDelResponse = processAxiosResponse(deletionResponse);
@@ -94,6 +97,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/sendgrid/util.js b/src/v0/destinations/sendgrid/util.js
index 1df34bfe69..7105c5cda5 100644
--- a/src/v0/destinations/sendgrid/util.js
+++ b/src/v0/destinations/sendgrid/util.js
@@ -445,6 +445,9 @@ const fetchCustomFields = async (destination) => {
const resonse = await httpGET(endpoint, requestOptions, {
destType: 'sendgrid',
feature: 'transformation',
+ endpointPath: '/marketing/field_definitions',
+ requestMethod: 'GET',
+ module: 'router',
});
const processedResponse = processAxiosResponse(resonse);
if (isHttpStatusSuccess(processedResponse.status)) {
diff --git a/src/v0/destinations/sendinblue/util.js b/src/v0/destinations/sendinblue/util.js
index 9ad37fc9b7..9fded8e493 100644
--- a/src/v0/destinations/sendinblue/util.js
+++ b/src/v0/destinations/sendinblue/util.js
@@ -60,6 +60,9 @@ const checkIfContactExists = async (identifier, apiKey) => {
const contactDetailsResponse = await httpGET(endpoint, requestOptions, {
destType: 'sendinblue',
feature: 'transformation',
+ endpointPath: '/contacts',
+ requestMethod: 'GET',
+ module: 'router',
});
const processedContactDetailsResponse = processAxiosResponse(contactDetailsResponse);
diff --git a/src/v0/destinations/sfmc/transform.js b/src/v0/destinations/sfmc/transform.js
index 7623d751f1..553ceb2828 100644
--- a/src/v0/destinations/sfmc/transform.js
+++ b/src/v0/destinations/sfmc/transform.js
@@ -44,7 +44,13 @@ const getToken = async (clientId, clientSecret, subdomain) => {
{
'Content-Type': JSON_MIME_TYPE,
},
- { destType: 'sfmc', feature: 'transformation' },
+ {
+ destType: 'sfmc',
+ feature: 'transformation',
+ endpointPath: '/token',
+ requestMethod: 'POST',
+ module: 'router',
+ },
);
if (resp && resp.data) {
return resp.data.access_token;
diff --git a/src/v0/destinations/snapchat_custom_audience/networkHandler.js b/src/v0/destinations/snapchat_custom_audience/networkHandler.js
index feedaea3e3..6044216293 100644
--- a/src/v0/destinations/snapchat_custom_audience/networkHandler.js
+++ b/src/v0/destinations/snapchat_custom_audience/networkHandler.js
@@ -43,6 +43,9 @@ const scAudienceProxyRequest = async (request) => {
const response = await httpSend(requestOptions, {
feature: 'proxy',
destType: 'snapchat_custom_audience',
+ endpointPath: '/segments/segmentId/users',
+ requestMethod: requestOptions?.method,
+ module: 'dataDelivery',
});
return response;
};
diff --git a/src/v0/destinations/sprig/deleteUsers.js b/src/v0/destinations/sprig/deleteUsers.js
index a886bbbafc..01044adcd1 100644
--- a/src/v0/destinations/sprig/deleteUsers.js
+++ b/src/v0/destinations/sprig/deleteUsers.js
@@ -49,7 +49,9 @@ const userDeletionHandler = async (userAttributes, config) => {
{
destType: 'sprig',
feature: 'deleteUsers',
- endpointPath: 'api.sprig.com/v2/purge/visitors',
+ endpointPath: '/purge/visitors',
+ requestMethod: 'POST',
+ module: 'deletion',
},
);
const handledDelResponse = processAxiosResponse(deletionResponse);
@@ -59,6 +61,7 @@ const userDeletionHandler = async (userAttributes, config) => {
handledDelResponse.status,
{
[tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(handledDelResponse.status),
+ [tags.TAG_NAMES.STATUS]: handledDelResponse.status,
},
handledDelResponse,
);
diff --git a/src/v0/destinations/the_trade_desk/networkHandler.js b/src/v0/destinations/the_trade_desk/networkHandler.js
index 5d443709b7..aebbfc0785 100644
--- a/src/v0/destinations/the_trade_desk/networkHandler.js
+++ b/src/v0/destinations/the_trade_desk/networkHandler.js
@@ -8,37 +8,28 @@ const { getSignatureHeader } = require('../../../cdk/v2/destinations/the_trade_d
const { isHttpStatusSuccess } = require('../../util/index');
const tags = require('../../util/tags');
const { JSON_MIME_TYPE } = require('../../util/constant');
-const {
- REAL_TIME_CONVERSION_ENDPOINT,
-} = require('../../../cdk/v2/destinations/the_trade_desk/config');
const proxyRequest = async (request) => {
const { endpoint, data, method, params, headers, config } = prepareProxyRequest(request);
- let ProxyHeaders = {
- ...headers,
- 'Content-Type': JSON_MIME_TYPE,
- };
-
- // For first party data flow
- if (endpoint !== REAL_TIME_CONVERSION_ENDPOINT) {
- if (!config?.advertiserSecretKey) {
- throw new PlatformError('Advertiser secret key is missing in destination config. Aborting');
- }
- if (!process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY) {
- throw new PlatformError('Data provider secret key is missing. Aborting');
- }
+ if (!config?.advertiserSecretKey) {
+ throw new PlatformError('Advertiser secret key is missing in destination config. Aborting');
+ }
- ProxyHeaders = {
- ...ProxyHeaders,
- TtdSignature: getSignatureHeader(data, config.advertiserSecretKey),
- 'TtdSignature-dp': getSignatureHeader(
- data,
- process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY,
- ),
- };
+ if (!process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY) {
+ throw new PlatformError('Data provider secret key is missing. Aborting');
}
+ const ProxyHeaders = {
+ ...headers,
+ 'Content-Type': JSON_MIME_TYPE,
+ TtdSignature: getSignatureHeader(data, config.advertiserSecretKey),
+ 'TtdSignature-dp': getSignatureHeader(
+ data,
+ process.env.THE_TRADE_DESK_DATA_PROVIDER_SECRET_KEY,
+ ),
+ };
+
const requestOptions = {
url: endpoint,
data,
@@ -46,7 +37,13 @@ const proxyRequest = async (request) => {
headers: ProxyHeaders,
method,
};
- const response = await httpSend(requestOptions, { feature: 'proxy', destType: 'the_trade_desk' });
+ const response = await httpSend(requestOptions, {
+ feature: 'proxy',
+ destType: 'the_trade_desk',
+ endpointPath: '/track/realtimeconversion',
+ requestMethod: 'POST',
+ module: 'dataDelivery',
+ });
return response;
};
@@ -70,7 +67,6 @@ const responseHandler = (responseParams) => {
// Trade desk first party data api returns 200 with an error in case of "Failed to parse TDID, DAID, UID2, IDL, EUID, or failed to decrypt UID2Token or EUIDToken"
// https://partner.thetradedesk.com/v3/portal/data/doc/post-data-advertiser-external
// {"FailedLines":[{"ErrorCode":"MissingUserId","Message":"Invalid DAID, item #1"}]}
- // For real time conversion api we don't have separate response handling, trade desk always return 400 for bad events.
if ('FailedLines' in response && response.FailedLines.length > 0) {
throw new AbortedError(
`Request failed with status: ${status} due to ${JSON.stringify(response)}`,
diff --git a/src/v0/destinations/tiktok_ads/transformV2.js b/src/v0/destinations/tiktok_ads/transformV2.js
index 91078dfe65..98f7d61e1e 100644
--- a/src/v0/destinations/tiktok_ads/transformV2.js
+++ b/src/v0/destinations/tiktok_ads/transformV2.js
@@ -40,7 +40,7 @@ const getTrackResponsePayload = (message, destConfig, event) => {
}
// if contents is not present but we have properties.products present which has fields with superset of contents fields
- if (payload.properties && !payload.properties.contents && message.properties.products) {
+ if (!payload.properties?.contents && message.properties?.products) {
// retreiving data from products only when contents is not present
payload.properties.contents = getContents(message, false);
}
diff --git a/src/v0/destinations/tiktok_ads_offline_events/config.js b/src/v0/destinations/tiktok_ads_offline_events/config.js
index 5d5e80c716..3c58b42a44 100644
--- a/src/v0/destinations/tiktok_ads_offline_events/config.js
+++ b/src/v0/destinations/tiktok_ads_offline_events/config.js
@@ -19,12 +19,24 @@ const CONFIG_CATEGORIES = {
const PARTNER_NAME = 'RudderStack';
const EVENT_NAME_MAPPING = {
+ 'addpaymentinfo': 'AddPaymentInfo',
+ 'addtocart': 'AddToCart',
+ 'addtowishlist': 'AddToWishlist',
+ 'checkout started': 'InitiateCheckout',
'checkout step completed': 'CompletePayment',
- contact: 'Contact',
- submitform: 'SubmitForm',
- subscribe: 'Subscribe',
+ 'clickbutton': 'ClickButton',
+ 'completeregistration': 'CompleteRegistration',
+ 'contact': 'Contact',
+ 'download': 'Download',
+ 'order completed': 'PlaceAnOrder',
+ 'payment info entered': 'AddPaymentInfo',
+ 'product added': 'AddToCart',
+ 'product added to wishlist': 'AddToWishlist',
+ 'search': 'Search',
+ 'submitform': 'SubmitForm',
+ 'subscribe': 'Subscribe',
+ 'viewcontent': 'ViewContent',
};
-
const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname);
module.exports = {
diff --git a/src/v0/destinations/trengo/transform.js b/src/v0/destinations/trengo/transform.js
index 06e5496a1e..01c5cfeb25 100644
--- a/src/v0/destinations/trengo/transform.js
+++ b/src/v0/destinations/trengo/transform.js
@@ -90,7 +90,13 @@ const lookupContact = async (term, destination) => {
Authorization: `Bearer ${destination.Config.apiToken}`,
},
},
- { destType: 'trengo', feature: 'transformation' },
+ {
+ destType: 'trengo',
+ feature: 'transformation',
+ endpointPath: '/contacts',
+ requestMethod: 'GET',
+ module: 'router',
+ },
);
} catch (err) {
// check if exists err.response && err.response.status else 500
diff --git a/src/v0/destinations/user/utils.js b/src/v0/destinations/user/utils.js
index 52fba2167e..f332d7a4a7 100644
--- a/src/v0/destinations/user/utils.js
+++ b/src/v0/destinations/user/utils.js
@@ -238,6 +238,8 @@ const createCompany = async (message, destination) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/companies/`,
+ requestMethod: 'POST',
+ module: 'router',
});
const data = processAxiosResponse(response);
return data.response;
@@ -279,6 +281,8 @@ const updateCompany = async (message, destination, company) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/companies/`,
+ requestMethod: 'PUT',
+ module: 'router',
});
const data = processAxiosResponse(response);
return data.response;
@@ -306,6 +310,8 @@ const getUserByUserKey = async (apiKey, userKey, appSubdomain) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/users/search`,
+ requestMethod: 'GET',
+ module: 'router',
});
const processedUserResponse = processAxiosResponse(userResponse);
if (processedUserResponse.status === 200) {
@@ -340,6 +346,8 @@ const getUserByEmail = async (apiKey, email, appSubdomain) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/users/search/?email`,
+ requestMethod: 'GET',
+ module: 'router',
});
const processedUserResponse = processAxiosResponse(userResponse);
@@ -379,6 +387,8 @@ const getUserByPhoneNumber = async (apiKey, phoneNumber, appSubdomain) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/users/search/?phone_number`,
+ requestMethod: 'GET',
+ module: 'router',
});
const processedUserResponse = processAxiosResponse(userResponse);
@@ -424,6 +434,8 @@ const getUserByCustomId = async (message, destination) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/users-by-id/`,
+ requestMethod: 'GET',
+ module: 'router',
});
const processedUserResponse = processAxiosResponse(userResponse);
@@ -460,6 +472,8 @@ const getCompanyByCustomId = async (message, destination) => {
destType: 'user',
feature: 'transformation',
endpointPath: `/companies-by-id/`,
+ requestMethod: 'GET',
+ module: 'router',
});
const processedUserResponse = processAxiosResponse(response);
if (processedUserResponse.status === 200) {
diff --git a/src/v0/destinations/wootric/util.js b/src/v0/destinations/wootric/util.js
index eb61a472cf..0ae0a4940b 100644
--- a/src/v0/destinations/wootric/util.js
+++ b/src/v0/destinations/wootric/util.js
@@ -47,6 +47,8 @@ const getAccessToken = async (destination) => {
destType: 'wootric',
feature: 'transformation',
endpointPath: `/oauth/token`,
+ requestMethod: 'POST',
+ module: 'router'
});
const processedAuthResponse = processAxiosResponse(wootricAuthResponse);
// If the request fails, throwing error.
@@ -100,6 +102,8 @@ const retrieveUserDetails = async (endUserId, externalId, accessToken) => {
destType: 'wootric',
feature: 'transformation',
endpointPath: `/v1/end_users/`,
+ requestMethod: 'GET',
+ module: 'router'
});
const processedUserResponse = processAxiosResponse(userResponse);
diff --git a/src/v0/destinations/yahoo_dsp/util.js b/src/v0/destinations/yahoo_dsp/util.js
index d41716935f..255f84d1c9 100644
--- a/src/v0/destinations/yahoo_dsp/util.js
+++ b/src/v0/destinations/yahoo_dsp/util.js
@@ -137,6 +137,9 @@ const getAccessToken = async (destination) => {
const dspAuthorisationData = await httpSend(request, {
destType: 'yahoo_dsp',
feature: 'transformation',
+ endpointPath: '/identity/oauth2/access_token',
+ requestMethod: 'POST',
+ module: 'router',
});
// If the request fails, throwing error.
if (dspAuthorisationData.success === false) {
diff --git a/src/v0/destinations/zendesk/transform.js b/src/v0/destinations/zendesk/transform.js
index bf2bc01ed2..5862014784 100644
--- a/src/v0/destinations/zendesk/transform.js
+++ b/src/v0/destinations/zendesk/transform.js
@@ -36,7 +36,7 @@ const tags = require('../../util/tags');
const { JSON_MIME_TYPE } = require('../../util/constant');
const CONTEXT_TRAITS_KEY_PATH = 'context.traits';
-
+const endpointPath = '/users/search.json';
function responseBuilder(message, headers, payload, endpoint) {
const response = defaultRequestConfig();
@@ -102,6 +102,9 @@ const payloadBuilderforUpdatingEmail = async (userId, headers, userEmail, baseEn
const res = await httpGET(url, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath: 'users/userId/identities',
+ requestMethod: 'POST',
+ module: 'router',
});
if (res?.response?.data?.count > 0) {
const { identities } = res.response.data;
@@ -147,6 +150,9 @@ async function createUserFields(url, config, newFields, fieldJson) {
const response = await myAxios.post(url, fieldData, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath: '/users/userId/identities',
+ requestMethod: 'POST',
+ module: 'router',
});
if (response.status !== 201) {
logger.debug(`${NAME}:: Failed to create User Field : `, field);
@@ -176,6 +182,8 @@ async function checkAndCreateUserFields(
const response = await myAxios.get(url, config, {
destType: 'zendesk',
feature: 'transformation',
+ requestMethod: 'POST',
+ module: 'router',
});
const fields = get(response.data, fieldJson);
if (response.data && fields) {
@@ -253,6 +261,9 @@ const getUserIdByExternalId = async (message, headers, baseEndpoint) => {
const resp = await httpGET(url, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath,
+ requestMethod: 'GET',
+ module: 'router',
});
if (resp?.response?.data?.count > 0) {
@@ -283,6 +294,9 @@ async function getUserId(message, headers, baseEndpoint, type) {
const resp = await myAxios.get(url, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath,
+ requestMethod: 'GET',
+ module: 'router',
});
if (!resp || !resp.data || resp.data.count === 0) {
logger.debug(`${NAME}:: User not found`);
@@ -307,6 +321,9 @@ async function isUserAlreadyAssociated(userId, orgId, headers, baseEndpoint) {
const response = await myAxios.get(url, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath: '/users/userId/organization_memberships.json',
+ requestMethod: 'GET',
+ module: 'router',
});
if (response?.data?.organization_memberships?.[0]?.organization_id === orgId) {
return true;
@@ -339,6 +356,9 @@ async function createUser(message, headers, destinationConfig, baseEndpoint, typ
const resp = await myAxios.post(url, payload, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath: '/users/create_or_update.json',
+ requestMethod: 'POST',
+ module: 'router',
});
if (!resp.data || !resp.data.user || !resp.data.user.id) {
@@ -420,6 +440,9 @@ async function createOrganization(message, category, headers, destinationConfig,
const resp = await myAxios.post(url, payload, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath: '/organizations/create_or_update.json',
+ requestMethod: 'POST',
+ module: 'router',
});
if (!resp.data || !resp.data.organization) {
@@ -488,6 +511,9 @@ async function processIdentify(message, destinationConfig, headers, baseEndpoint
const response = await myAxios.get(membershipUrl, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath: '/users/userId/organization_memberships.json',
+ requestMethod: 'GET',
+ module: 'router',
});
if (
response.data &&
@@ -535,6 +561,9 @@ async function processTrack(message, destinationConfig, headers, baseEndpoint) {
const userResponse = await myAxios.get(url, config, {
destType: 'zendesk',
feature: 'transformation',
+ endpointPath,
+ requestMethod: 'GET',
+ module: 'router',
});
if (!get(userResponse, 'data.users.0.id') || userResponse.data.count === 0) {
const { zendeskUserId, email } = await createUser(
diff --git a/src/v0/util/facebookUtils/index.js b/src/v0/util/facebookUtils/index.js
index 4c09518559..7fa1e898fe 100644
--- a/src/v0/util/facebookUtils/index.js
+++ b/src/v0/util/facebookUtils/index.js
@@ -147,7 +147,7 @@ const getContentType = (message, defaultValue, categoryToContent, destinationNam
return integrationsObj.contentType;
}
- let { category } = properties;
+ let { category } = properties || {};
if (!category) {
const { products } = properties;
if (products && products.length > 0 && Array.isArray(products) && isObject(products[0])) {
diff --git a/src/v0/util/index.js b/src/v0/util/index.js
index 49ef39969e..0cc66b2d7a 100644
--- a/src/v0/util/index.js
+++ b/src/v0/util/index.js
@@ -33,6 +33,7 @@ const {
AUTH_STATUS_INACTIVE,
} = require('../../adapters/networkhandler/authConstants');
const { FEATURE_FILTER_CODE, FEATURE_GZIP_SUPPORT } = require('./constant');
+const { CommonUtils } = require('../../util/common');
// ========================================================================
// INLINERS
@@ -2136,6 +2137,87 @@ const parseConfigArray = (arr, key) => {
return arr.map((item) => item[key]);
};
+/**
+ * Finds an existing batch based on metadata JobIds from the provided batch and metadataMap.
+ * @param {*} batch
+ * @param {*} metadataMap The map containing metadata items indexed by JobIds.
+ * @returns
+ */
+const findExistingBatch = (batch, metadataMap) => {
+ const existingMetadataItem = batch.metadata.find((metadataItem) =>
+ metadataMap.has(metadataItem.jobId),
+ );
+ return existingMetadataItem ? metadataMap.get(existingMetadataItem.jobId) : null;
+};
+
+/**
+ * Removes duplicate metadata within each merged batch object.
+ * @param {*} mergedBatches An array of merged batch objects.
+ */
+const removeDuplicateMetadata = (mergedBatches) => {
+ mergedBatches.forEach((batch) => {
+ const metadataSet = new Set();
+ // eslint-disable-next-line no-param-reassign
+ batch.metadata = batch.metadata.filter((metadataItem) => {
+ if (!metadataSet.has(metadataItem.jobId)) {
+ metadataSet.add(metadataItem.jobId);
+ return true;
+ }
+ return false;
+ });
+ });
+};
+
+/**
+ * Combines batched requests with the same JobIds.
+ * @param {*} inputBatches The array of batched request objects.
+ * @returns The combined batched requests with merged JobIds.
+ *
+ */
+const combineBatchRequestsWithSameJobIds = (inputBatches) => {
+ const combineBatches = (batches) => {
+ const clonedBatches = [...batches];
+ const mergedBatches = [];
+ const metadataMap = new Map();
+
+ clonedBatches.forEach((batch) => {
+ const existingBatch = findExistingBatch(batch, metadataMap);
+
+ if (existingBatch) {
+ // Merge batchedRequests arrays
+ existingBatch.batchedRequest = [
+ ...CommonUtils.toArray(existingBatch.batchedRequest),
+ ...CommonUtils.toArray(batch.batchedRequest),
+ ];
+
+ // Merge metadata
+ batch.metadata.forEach((metadataItem) => {
+ if (!metadataMap.has(metadataItem.jobId)) {
+ metadataMap.set(metadataItem.jobId, existingBatch);
+ }
+ existingBatch.metadata.push(metadataItem);
+ });
+ } else {
+ mergedBatches.push(batch);
+ batch.metadata.forEach((metadataItem) => {
+ metadataMap.set(metadataItem.jobId, batch);
+ });
+ }
+ });
+
+ // Remove duplicate metadata within each merged object
+ removeDuplicateMetadata(mergedBatches);
+
+ return mergedBatches;
+ };
+ // We need to run this twice because in first pass some batches might not get merged
+ // and in second pass they might get merged
+ // Example: [[{jobID:1}, {jobID:2}], [{jobID:3}], [{jobID:1}, {jobID:3}]]
+ // 1st pass: [[{jobID:1}, {jobID:2}, {jobID:3}], [{jobID:3}]]
+ // 2nd pass: [[{jobID:1}, {jobID:2}, {jobID:3}]]
+ return combineBatches(combineBatches(inputBatches));
+};
+
// ========================================================================
// EXPORTS
// ========================================================================
@@ -2249,4 +2331,7 @@ module.exports = {
isNewStatusCodesAccepted,
IsGzipSupported,
parseConfigArray,
+ findExistingBatch,
+ removeDuplicateMetadata,
+ combineBatchRequestsWithSameJobIds,
};
diff --git a/src/v0/util/index.test.js b/src/v0/util/index.test.js
index 1c6b34eca6..4dc6255691 100644
--- a/src/v0/util/index.test.js
+++ b/src/v0/util/index.test.js
@@ -2,7 +2,12 @@ const { TAG_NAMES } = require('@rudderstack/integrations-lib');
const utilities = require('.');
const { getFuncTestData } = require('../../../test/testHelper');
const { FilteredEventsError } = require('./errorTypes');
-const { hasCircularReference, flattenJson, generateExclusionList } = require('./index');
+const {
+ hasCircularReference,
+ flattenJson,
+ generateExclusionList,
+ combineBatchRequestsWithSameJobIds,
+} = require('./index');
// Names of the utility functions to test
const functionNames = [
@@ -166,3 +171,288 @@ describe('generateExclusionList', () => {
expect(result).toEqual(expected);
});
});
+
+describe('Unit test cases for combineBatchRequestsWithSameJobIds', () => {
+ it('Combine batch request with same jobIds', async () => {
+ const input = [
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint1',
+ },
+ metadata: [
+ {
+ jobId: 1,
+ },
+ {
+ jobId: 4,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint2',
+ },
+ metadata: [
+ {
+ jobId: 3,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint1',
+ },
+ metadata: [
+ {
+ jobId: 5,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint3',
+ },
+ metadata: [
+ {
+ jobId: 1,
+ },
+ {
+ jobId: 3,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint2',
+ },
+ metadata: [
+ {
+ jobId: 6,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ ];
+
+ const expectedOutput = [
+ {
+ batchedRequest: [
+ {
+ endpoint: 'https://endpoint1',
+ },
+ {
+ endpoint: 'https://endpoint3',
+ },
+ {
+ endpoint: 'https://endpoint2',
+ },
+ ],
+ metadata: [
+ {
+ jobId: 1,
+ },
+ {
+ jobId: 4,
+ },
+ {
+ jobId: 3,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint1',
+ },
+ metadata: [
+ {
+ jobId: 5,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint2',
+ },
+ metadata: [
+ {
+ jobId: 6,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ ];
+ expect(combineBatchRequestsWithSameJobIds(input)).toEqual(expectedOutput);
+ });
+
+ it('Each batchRequest contains unique jobIds (no event multiplexing)', async () => {
+ const input = [
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint1',
+ },
+ metadata: [
+ {
+ jobId: 1,
+ },
+ {
+ jobId: 4,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint3',
+ },
+ metadata: [
+ {
+ jobId: 2,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint3',
+ },
+ metadata: [
+ {
+ jobId: 5,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ ];
+
+ const expectedOutput = [
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint1',
+ },
+
+ metadata: [
+ {
+ jobId: 1,
+ },
+ {
+ jobId: 4,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint3',
+ },
+ metadata: [
+ {
+ jobId: 2,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ {
+ batchedRequest: {
+ endpoint: 'https://endpoint3',
+ },
+ metadata: [
+ {
+ jobId: 5,
+ },
+ ],
+ batched: true,
+ statusCode: 200,
+ destination: {
+ Config: {
+ key: 'value',
+ },
+ },
+ },
+ ];
+ expect(combineBatchRequestsWithSameJobIds(input)).toEqual(expectedOutput);
+ });
+});
diff --git a/src/v0/util/tags.js b/src/v0/util/tags.js
index 18f00f963f..dce8c0a338 100644
--- a/src/v0/util/tags.js
+++ b/src/v0/util/tags.js
@@ -13,6 +13,7 @@ const TAG_NAMES = {
DESTINATION_ID: 'destinationId',
WORKSPACE_ID: 'workspaceId',
SOURCE_ID: 'sourceId',
+ STATUS: 'statusCode',
};
const MODULES = {
@@ -51,7 +52,7 @@ const ERROR_TYPES = {
OAUTH_SECRET: 'oAuthSecret',
UNSUPPORTED: 'unsupported',
REDIS: 'redis',
- FILTERED: 'filtered'
+ FILTERED: 'filtered',
};
const METADATA = {
diff --git a/test/apitests/data_scenarios/destination/batch/failure_batch.json b/test/apitests/data_scenarios/destination/batch/failure_batch.json
index 8063bc74a1..6352ca1a11 100644
--- a/test/apitests/data_scenarios/destination/batch/failure_batch.json
+++ b/test/apitests/data_scenarios/destination/batch/failure_batch.json
@@ -1051,125 +1051,314 @@
},
"output": [
{
- "metadata": {
- "userId": "<<>>testUser<<>>testUser",
- "jobId": 2,
- "sourceId": "27O0bmEEx3GgfmEhZHUcPwJQVWC",
- "destinationId": "2JK3ACpBjq9AmvUbxR1u2pDPSYR",
- "attemptNum": 0,
- "receivedAt": "2022-12-24T17:29:00.699+05:30",
- "createdAt": "2022-12-24T11:59:03.125Z",
- "firstAttemptedAt": "",
- "transformAt": "processor",
- "workspaceId": "27O0bhB6p5ehfOWeeZlOSsSDTLg",
- "secret": null,
- "jobsT": {
- "UUID": "aaa8b7c4-2600-478b-b275-01740e1ef50c",
- "JobID": 2,
- "UserID": "<<>>testUser<<>>testUser",
- "CreatedAt": "2022-12-24T11:59:03.125515Z",
- "ExpireAt": "2022-12-24T11:59:03.125515Z",
- "CustomVal": "AM",
- "EventCount": 1,
- "EventPayload": {
- "body": {
- "XML": {},
- "FORM": {},
- "JSON": {
- "events": [
- {
- "ip": "[::1]",
- "time": 1671883143047,
- "library": "rudderstack",
- "user_id": "testUser",
- "device_id": "anon-id",
- "insert_id": "14642496-9a12-4db7-b0f2-9a336cf6cea9",
- "event_type": "Product Added",
- "session_id": -1,
- "user_properties": {
- "email": "test.c97@gmail.com",
- "phone": "+919876543210",
- "gender": "Male",
- "lastName": "Rudderlabs",
- "firstName": "test"
- },
- "event_properties": {
- "sku": "F15",
- "url": "https://www.website.com/product/path",
- "name": "Game",
- "brand": "Gamepro",
- "price": 13.49,
- "coupon": "DISC21",
- "variant": "111",
- "category": "Games",
- "position": 1,
- "quantity": 11,
- "image_url": "https://www.website.com/product/path.png",
- "product_id": "123"
+ "metadata": [
+ {
+ "userId": "<<>>testUser<<>>testUser",
+ "jobId": 2,
+ "sourceId": "27O0bmEEx3GgfmEhZHUcPwJQVWC",
+ "destinationId": "2JK3ACpBjq9AmvUbxR1u2pDPSYR",
+ "attemptNum": 0,
+ "receivedAt": "2022-12-24T17:29:00.699+05:30",
+ "createdAt": "2022-12-24T11:59:03.125Z",
+ "firstAttemptedAt": "",
+ "transformAt": "processor",
+ "workspaceId": "27O0bhB6p5ehfOWeeZlOSsSDTLg",
+ "secret": null,
+ "jobsT": {
+ "UUID": "aaa8b7c4-2600-478b-b275-01740e1ef50c",
+ "JobID": 2,
+ "UserID": "<<>>testUser<<>>testUser",
+ "CreatedAt": "2022-12-24T11:59:03.125515Z",
+ "ExpireAt": "2022-12-24T11:59:03.125515Z",
+ "CustomVal": "AM",
+ "EventCount": 1,
+ "EventPayload": {
+ "body": {
+ "XML": {},
+ "FORM": {},
+ "JSON": {
+ "events": [
+ {
+ "ip": "[::1]",
+ "time": 1671883143047,
+ "library": "rudderstack",
+ "user_id": "testUser",
+ "device_id": "anon-id",
+ "insert_id": "14642496-9a12-4db7-b0f2-9a336cf6cea9",
+ "event_type": "Product Added",
+ "session_id": -1,
+ "user_properties": {
+ "email": "test.c97@gmail.com",
+ "phone": "+919876543210",
+ "gender": "Male",
+ "lastName": "Rudderlabs",
+ "firstName": "test"
+ },
+ "event_properties": {
+ "sku": "F15",
+ "url": "https://www.website.com/product/path",
+ "name": "Game",
+ "brand": "Gamepro",
+ "price": 13.49,
+ "coupon": "DISC21",
+ "variant": "111",
+ "category": "Games",
+ "position": 1,
+ "quantity": 11,
+ "image_url": "https://www.website.com/product/path.png",
+ "product_id": "123"
+ }
}
+ ],
+ "api_key": "dummyApiKey",
+ "options": {
+ "min_id_length": 1
}
- ],
- "api_key": "dummyApiKey",
- "options": {
- "min_id_length": 1
- }
+ },
+ "JSON_ARRAY": {}
},
- "JSON_ARRAY": {}
+ "type": "REST",
+ "files": {},
+ "method": "POST",
+ "params": {},
+ "userId": "anon-id",
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "version": "1",
+ "endpoint": "https://api2.amplitude.com/2/httpapi"
},
- "type": "REST",
- "files": {},
- "method": "POST",
- "params": {},
- "userId": "anon-id",
- "headers": {
- "Content-Type": "application/json"
+ "PayloadSize": 1133,
+ "LastJobStatus": {
+ "JobID": 0,
+ "JobState": "",
+ "AttemptNum": 0,
+ "ExecTime": "0001-01-01T00:00:00Z",
+ "RetryTime": "0001-01-01T00:00:00Z",
+ "ErrorCode": "",
+ "ErrorResponse": null,
+ "Parameters": null,
+ "WorkspaceId": ""
},
- "version": "1",
- "endpoint": "https://api2.amplitude.com/2/httpapi"
- },
- "PayloadSize": 1133,
- "LastJobStatus": {
- "JobID": 0,
- "JobState": "",
- "AttemptNum": 0,
- "ExecTime": "0001-01-01T00:00:00Z",
- "RetryTime": "0001-01-01T00:00:00Z",
- "ErrorCode": "",
- "ErrorResponse": null,
- "Parameters": null,
- "WorkspaceId": ""
- },
- "Parameters": {
- "record_id": null,
- "source_id": "27O0bmEEx3GgfmEhZHUcPwJQVWC",
- "event_name": "Product Added",
- "event_type": "track",
- "message_id": "14642496-9a12-4db7-b0f2-9a336cf6cea9",
- "received_at": "2022-12-24T17:29:00.699+05:30",
- "workspaceId": "27O0bhB6p5ehfOWeeZlOSsSDTLg",
- "transform_at": "processor",
- "source_job_id": "",
- "destination_id": "2JK3ACpBjq9AmvUbxR1u2pDPSYR",
- "gateway_job_id": 1,
- "source_task_id": "",
- "source_batch_id": "",
- "source_category": "",
- "source_job_run_id": "",
- "source_task_run_id": "",
- "source_definition_id": "1b6gJdqOPOCadT3cddw8eidV591",
- "destination_definition_id": ""
+ "Parameters": {
+ "record_id": null,
+ "source_id": "27O0bmEEx3GgfmEhZHUcPwJQVWC",
+ "event_name": "Product Added",
+ "event_type": "track",
+ "message_id": "14642496-9a12-4db7-b0f2-9a336cf6cea9",
+ "received_at": "2022-12-24T17:29:00.699+05:30",
+ "workspaceId": "27O0bhB6p5ehfOWeeZlOSsSDTLg",
+ "transform_at": "processor",
+ "source_job_id": "",
+ "destination_id": "2JK3ACpBjq9AmvUbxR1u2pDPSYR",
+ "gateway_job_id": 1,
+ "source_task_id": "",
+ "source_batch_id": "",
+ "source_category": "",
+ "source_job_run_id": "",
+ "source_task_run_id": "",
+ "source_definition_id": "1b6gJdqOPOCadT3cddw8eidV591",
+ "destination_definition_id": ""
+ },
+ "WorkspaceId": "27O0bhB6p5ehfOWeeZlOSsSDTLg"
},
- "WorkspaceId": "27O0bhB6p5ehfOWeeZlOSsSDTLg"
- },
- "workerAssignedTime": "2022-12-24T17:29:04.051596+05:30"
- },
+ "workerAssignedTime": "2022-12-24T17:29:04.051596+05:30"
+ }
+ ],
"batched": false,
"statusCode": 400,
"statTags": {
"errorCategory": "dataValidation",
"errorType": "instrumentation"
},
- "error": "Both userId and deviceId cannot be undefined"
+ "error": "Both userId and deviceId cannot be undefined",
+ "destination": {
+ "ID": "2JK3ACpBjq9AmvUbxR1u2pDPSYR",
+ "Name": "Amplitude-2",
+ "DestinationDefinition": {
+ "ID": "1QGzO4fWSyq3lsyFHf4eQAMDSr9",
+ "Name": "AM",
+ "DisplayName": "Amplitude",
+ "Config": {
+ "destConfig": {
+ "android": [
+ "eventUploadPeriodMillis",
+ "eventUploadThreshold",
+ "useNativeSDK",
+ "enableLocationListening",
+ "trackSessionEvents",
+ "useAdvertisingIdForDeviceId"
+ ],
+ "defaultConfig": [
+ "apiKey",
+ "groupTypeTrait",
+ "groupValueTrait",
+ "trackAllPages",
+ "trackCategorizedPages",
+ "trackNamedPages",
+ "traitsToIncrement",
+ "traitsToSetOnce",
+ "traitsToAppend",
+ "traitsToPrepend",
+ "trackProductsOnce",
+ "trackRevenuePerProduct",
+ "versionName",
+ "apiSecret",
+ "residencyServer",
+ "blacklistedEvents",
+ "whitelistedEvents",
+ "eventFilteringOption",
+ "mapDeviceBrand"
+ ],
+ "flutter": [
+ "eventUploadPeriodMillis",
+ "eventUploadThreshold",
+ "useNativeSDK",
+ "enableLocationListening",
+ "trackSessionEvents",
+ "useAdvertisingIdForDeviceId",
+ "useIdfaAsDeviceId"
+ ],
+ "ios": [
+ "eventUploadPeriodMillis",
+ "eventUploadThreshold",
+ "useNativeSDK",
+ "trackSessionEvents",
+ "useIdfaAsDeviceId"
+ ],
+ "reactnative": [
+ "eventUploadPeriodMillis",
+ "eventUploadThreshold",
+ "useNativeSDK",
+ "enableLocationListening",
+ "trackSessionEvents",
+ "useAdvertisingIdForDeviceId",
+ "useIdfaAsDeviceId"
+ ],
+ "web": [
+ "useNativeSDK",
+ "preferAnonymousIdForDeviceId",
+ "deviceIdFromUrlParam",
+ "forceHttps",
+ "trackGclid",
+ "trackReferrer",
+ "saveParamsReferrerOncePerSession",
+ "trackUtmProperties",
+ "unsetParamsReferrerOnNewSession",
+ "batchEvents",
+ "eventUploadPeriodMillis",
+ "eventUploadThreshold",
+ "oneTrustCookieCategories"
+ ]
+ },
+ "excludeKeys": [],
+ "includeKeys": [
+ "apiKey",
+ "groupTypeTrait",
+ "groupValueTrait",
+ "trackAllPages",
+ "trackCategorizedPages",
+ "trackNamedPages",
+ "traitsToIncrement",
+ "traitsToSetOnce",
+ "traitsToAppend",
+ "traitsToPrepend",
+ "trackProductsOnce",
+ "trackRevenuePerProduct",
+ "preferAnonymousIdForDeviceId",
+ "deviceIdFromUrlParam",
+ "forceHttps",
+ "trackGclid",
+ "trackReferrer",
+ "saveParamsReferrerOncePerSession",
+ "trackUtmProperties",
+ "unsetParamsReferrerOnNewSession",
+ "batchEvents",
+ "eventUploadPeriodMillis",
+ "eventUploadThreshold",
+ "versionName",
+ "enableLocationListening",
+ "useAdvertisingIdForDeviceId",
+ "trackSessionEvents",
+ "useIdfaAsDeviceId",
+ "blacklistedEvents",
+ "whitelistedEvents",
+ "oneTrustCookieCategories",
+ "eventFilteringOption",
+ "mapDeviceBrand"
+ ],
+ "saveDestinationResponse": true,
+ "secretKeys": ["apiKey", "apiSecret"],
+ "supportedMessageTypes": ["alias", "group", "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": "dummyApiKey",
+ "apiSecret": "",
+ "blacklistedEvents": [
+ {
+ "eventName": ""
+ }
+ ],
+ "eventFilteringOption": "disable",
+ "groupTypeTrait": "",
+ "groupValueTrait": "",
+ "mapDeviceBrand": false,
+ "residencyServer": "standard",
+ "trackAllPages": false,
+ "trackCategorizedPages": true,
+ "trackNamedPages": true,
+ "trackProductsOnce": false,
+ "trackRevenuePerProduct": false,
+ "traitsToAppend": [
+ {
+ "traits": ""
+ }
+ ],
+ "traitsToIncrement": [
+ {
+ "traits": ""
+ }
+ ],
+ "traitsToPrepend": [
+ {
+ "traits": ""
+ }
+ ],
+ "traitsToSetOnce": [
+ {
+ "traits": ""
+ }
+ ],
+ "versionName": "",
+ "whitelistedEvents": [
+ {
+ "eventName": ""
+ }
+ ]
+ },
+ "Enabled": true,
+ "WorkspaceID": "27O0bhB6p5ehfOWeeZlOSsSDTLg",
+ "Transformations": [],
+ "IsProcessorEnabled": true,
+ "RevisionID": "2JMKUgZX3b8sbtDSZrkUB7okeOY"
+ }
},
{
"batchedRequest": {
diff --git a/test/integrations/common/criteo/network.ts b/test/integrations/common/criteo/network.ts
new file mode 100644
index 0000000000..cd5e1ca1e8
--- /dev/null
+++ b/test/integrations/common/criteo/network.ts
@@ -0,0 +1,72 @@
+const headers = {
+ Authorization: 'Bearer success_access_token',
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ 'User-Agent': 'RudderLabs',
+};
+const params = { destination: 'criteo_audience' };
+const method = 'PATCH';
+const commonData = {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+};
+
+export const networkCallsData = [
+ {
+ description: 'Mock response depicting expired access token error',
+ httpReq: {
+ url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist/expiredAccessToken',
+ data: commonData,
+ params,
+ headers,
+ method,
+ },
+ httpRes: {
+ code: '400',
+ data: {
+ errors: [
+ {
+ traceIdentifier: '80a1a0ba3981b04da847d05700752c77',
+ type: 'authorization',
+ code: 'authorization-token-expired',
+ instance: '/2022-10/audiences/123/contactlist',
+ title: 'The authorization token has expired',
+ },
+ ],
+ },
+ status: 401,
+ },
+ },
+ {
+ description: 'Mock response depicting invalid access token error',
+ httpReq: {
+ url: 'https://api.criteo.com/2022-10/audiences/34895/contactlist/invalidAccessToken',
+ data: commonData,
+ params,
+ headers,
+ method,
+ },
+ httpRes: {
+ code: '400',
+ data: {
+ errors: [
+ {
+ traceIdentifier: '80a1a0ba3981b04da847d05700752c77',
+ type: 'authorization',
+ code: 'authorization-token-invalid',
+ instance: '/2022-10/audiences/123/contactlist',
+ title: 'The authorization header is invalid',
+ },
+ ],
+ },
+ status: 401,
+ },
+ },
+];
diff --git a/test/integrations/common/network.ts b/test/integrations/common/network.ts
index 8f80e406ae..8b0ed16c72 100644
--- a/test/integrations/common/network.ts
+++ b/test/integrations/common/network.ts
@@ -17,7 +17,18 @@ export const networkCallsData = [
},
},
{
- description: 'Mock response depicting INTERNAL SERVER ERROR error',
+ description: 'Mock response depicting INTERNAL SERVER ERROR error with post method',
+ httpReq: {
+ method: 'post',
+ url: 'https://random_test_url/test_for_internal_server_error',
+ },
+ httpRes: {
+ data: 'Internal Server Error',
+ status: 500,
+ },
+ },
+ {
+ description: 'Mock response depicting INTERNAL SERVER ERROR error with patch method',
httpReq: {
method: 'post',
url: 'https://random_test_url/test_for_internal_server_error',
@@ -59,4 +70,15 @@ export const networkCallsData = [
data: null,
},
},
+ {
+ description: 'Mock response depicting TOO MANY REQUESTS error with patch method',
+ httpReq: {
+ method: 'patch',
+ url: 'https://random_test_url/test_for_too_many_requests',
+ },
+ httpRes: {
+ data: {},
+ status: 429,
+ },
+ },
];
diff --git a/test/integrations/component.test.ts b/test/integrations/component.test.ts
index aaaa536d91..388c283c61 100644
--- a/test/integrations/component.test.ts
+++ b/test/integrations/component.test.ts
@@ -54,7 +54,7 @@ if (opts.generate === 'true') {
let server: Server;
-const INTEGRATIONS_WITH_UPDATED_TEST_STRUCTURE = ['klaviyo', 'campaign_manager'];
+const INTEGRATIONS_WITH_UPDATED_TEST_STRUCTURE = ['klaviyo', 'campaign_manager', 'criteo_audience'];
beforeAll(async () => {
initaliseReport();
diff --git a/test/integrations/destinations/af/processor/data.ts b/test/integrations/destinations/af/processor/data.ts
index 8b639f45c0..d0fd29b089 100644
--- a/test/integrations/destinations/af/processor/data.ts
+++ b/test/integrations/destinations/af/processor/data.ts
@@ -45,6 +45,7 @@ export const data = [
destination: {
Config: { devKey: 'ef1d42390426e3f7c90ac78272e74344', androidAppId: 'appId' },
Enabled: true,
+ addPropertiesAtRoot: false,
},
},
],
@@ -118,6 +119,7 @@ export const data = [
Config: {
devKey: 'ef1d42390426e3f7c90ac78272e74344',
androidAppId: 'com.rudderlabs.javascript',
+ addPropertiesAtRoot: false,
},
Enabled: true,
},
@@ -305,6 +307,7 @@ export const data = [
Config: {
devKey: 'ef1d42390426e3f7c90ac78272e74344',
androidAppId: 'com.rudderlabs.javascript',
+ addPropertiesAtRoot: false,
},
Enabled: true,
},
@@ -1532,4 +1535,306 @@ export const data = [
},
},
},
+ {
+ name: 'af',
+ description: 'Place Properties at root level Page Call',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ channel: 'web',
+ context: {
+ externalId: [{ type: 'appsflyerExternalId', id: 'afUid' }],
+ app: {
+ build: '1.0.0',
+ name: 'RudderLabs JavaScript SDK',
+ namespace: 'com.rudderlabs.javascript',
+ version: '1.0.0',
+ },
+ traits: {
+ email: 'testhubspot2@email.com',
+ name: 'Test Hubspot',
+ anonymousId: '12345',
+ },
+ library: { name: 'RudderLabs JavaScript SDK', version: '1.0.0' },
+ userAgent:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
+ locale: 'en-GB',
+ ip: '0.0.0.0',
+ os: { name: 'android', version: '' },
+ screen: { density: 2 },
+ },
+ type: 'page',
+ messageId: 'e8585d9a-7137-4223-b295-68ab1b17dad7',
+ originalTimestamp: '2019-10-15T09:35:31.289Z',
+ anonymousId: '00000000000000000000000000',
+ userId: '12345',
+ properties: { path: '', referrer: '', search: '', title: '', url: '' },
+ name: 'ApplicationLoaded',
+ sentAt: '2019-10-14T11:15:53.296Z',
+ integrations: { AF: { af_uid: 'afUid' } },
+ },
+ destination: {
+ Config: {
+ devKey: 'ef1d42390426e3f7c90ac78272e74344',
+ androidAppId: 'com.rudderlabs.javascript',
+ sharingFilter: 'all',
+ addPropertiesAtRoot: true,
+ },
+ Enabled: true,
+ },
+ },
+ ],
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ endpoint: 'https://api2.appsflyer.com/inappevent/com.rudderlabs.javascript',
+ headers: {
+ 'Content-Type': 'application/json',
+ authentication: 'ef1d42390426e3f7c90ac78272e74344',
+ },
+ method: 'POST',
+ params: {},
+ body: {
+ JSON: {
+ app_version_name: '1.0.0',
+ bundleIdentifier: 'com.rudderlabs.javascript',
+ customer_user_id: '12345',
+ eventValue: '{"path":"","referrer":"","search":"","title":"","url":""}',
+ eventName: 'page',
+ appsflyer_id: 'afUid',
+ os: '',
+ ip: '0.0.0.0',
+ sharing_filter: 'all',
+ },
+ XML: {},
+ JSON_ARRAY: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: 'af',
+ description: 'Place properties at root level track call',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ channel: 'web',
+ context: {
+ externalId: [{ type: 'appsflyerExternalId', id: 'afUid' }],
+ app: {
+ build: '1.0.0',
+ name: 'RudderLabs JavaScript SDK',
+ namespace: 'com.rudderlabs.javascript',
+ version: '1.0.0',
+ },
+ traits: { email: 'testhubspot2@email.com', name: 'Test Hubspot' },
+ library: { name: 'RudderLabs JavaScript SDK', version: '1.0.0' },
+ userAgent:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36',
+ locale: 'en-GB',
+ ip: '0.0.0.0',
+ os: { name: 'android', version: '' },
+ screen: { density: 2 },
+ },
+ type: 'track',
+ messageId: '08829772-d991-427c-b976-b4c4f4430b4e',
+ originalTimestamp: '2019-10-15T09:35:31.291Z',
+ anonymousId: '00000000000000000000000000',
+ userId: '12345',
+ event: 'test track event HS',
+ properties: { user_actual_role: 'system_admin, system_user', user_actual_id: 12345 },
+ sentAt: '2019-10-14T11:15:53.296Z',
+ integrations: { AF: { af_uid: 'afUid' } },
+ },
+ destination: {
+ Config: {
+ devKey: 'ef1d42390426e3f7c90ac78272e74344',
+ androidAppId: 'com.rudderlabs.javascript',
+ addPropertiesAtRoot: true,
+ },
+ Enabled: true,
+ },
+ },
+ ],
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://api2.appsflyer.com/inappevent/com.rudderlabs.javascript',
+ headers: {
+ 'Content-Type': 'application/json',
+ authentication: 'ef1d42390426e3f7c90ac78272e74344',
+ },
+ params: {},
+ body: {
+ JSON: {
+ eventValue:
+ '{"user_actual_role":"system_admin, system_user","user_actual_id":12345}',
+ eventName: 'test track event HS',
+ customer_user_id: '12345',
+ ip: '0.0.0.0',
+ os: '',
+ appsflyer_id: 'afUid',
+ app_version_name: '1.0.0',
+ bundleIdentifier: 'com.rudderlabs.javascript',
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: 'af',
+ description: 'Place properties at root track call with af data',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Order Completed',
+ sentAt: '2020-08-14T05:30:30.118Z',
+ context: {
+ externalId: [{ type: 'appsflyerExternalId', id: 'afUid' }],
+ source: 'test',
+ app: { namespace: 'com.rudderlabs.javascript' },
+ os: { name: 'android' },
+ traits: { anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1' },
+ library: { name: 'rudder-sdk-ruby-sync', version: '1.0.6' },
+ },
+ messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9',
+ timestamp: '2020-08-14T05:30:30.118Z',
+ properties: {
+ tax: 2,
+ total: 27.5,
+ coupon: 'hasbros',
+ revenue: 48,
+ price: 25,
+ quantity: 2,
+ currency: 'ZAR',
+ discount: 2.5,
+ order_id: '50314b8e9bcf000000000000',
+ products: [
+ {
+ sku: '45790-32',
+ url: 'https://www.example.com/product/path',
+ name: 'Monopoly: 3rd Edition',
+ price: 19,
+ category: 'Games',
+ quantity: 1,
+ image_url: 'https:///www.example.com/product/path.jpg',
+ product_id: '507f1f77bcf86cd799439011',
+ },
+ {
+ sku: '46493-32',
+ name: 'Uno Card Game',
+ price: 3,
+ category: 'Games',
+ quantity: 2,
+ product_id: '505bd76785ebb509fc183733',
+ },
+ ],
+ shipping: 3,
+ subtotal: 22.5,
+ affiliation: 'Google Store',
+ checkout_id: 'fksdjfsdjfisjf9sdfjsd9f',
+ },
+ anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1',
+ integrations: { AF: { af_uid: 'afUid' } },
+ },
+ destination: {
+ Config: {
+ devKey: 'abcde',
+ androidAppId: 'com.rudderlabs.javascript',
+ groupTypeTrait: 'email',
+ groupValueTrait: 'age',
+ trackProductsOnce: false,
+ trackRevenuePerProduct: false,
+ addPropertiesAtRoot: true,
+ },
+ },
+ },
+ ],
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://api2.appsflyer.com/inappevent/com.rudderlabs.javascript',
+ headers: { 'Content-Type': 'application/json', authentication: 'abcde' },
+ params: {},
+ body: {
+ JSON: {
+ bundleIdentifier: 'com.rudderlabs.javascript',
+ eventValue:
+ '{"tax":2,"total":27.5,"coupon":"hasbros","revenue":48,"price":25,"quantity":2,"currency":"ZAR","discount":2.5,"order_id":"50314b8e9bcf000000000000","products":[{"sku":"45790-32","url":"https://www.example.com/product/path","name":"Monopoly: 3rd Edition","price":19,"category":"Games","quantity":1,"image_url":"https:///www.example.com/product/path.jpg","product_id":"507f1f77bcf86cd799439011"},{"sku":"46493-32","name":"Uno Card Game","price":3,"category":"Games","quantity":2,"product_id":"505bd76785ebb509fc183733"}],"shipping":3,"subtotal":22.5,"affiliation":"Google Store","checkout_id":"fksdjfsdjfisjf9sdfjsd9f","af_revenue":48,"af_price":[19,3],"af_quantity":[1,2],"af_order_id":"50314b8e9bcf000000000000","af_content_id":["507f1f77bcf86cd799439011","505bd76785ebb509fc183733"]}',
+ eventName: 'Order Completed',
+ eventCurrency: 'ZAR',
+ eventTime: '2020-08-14T05:30:30.118Z',
+ appsflyer_id: 'afUid',
+ },
+ XML: {},
+ JSON_ARRAY: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
];
diff --git a/test/integrations/destinations/am/batch/data.ts b/test/integrations/destinations/am/batch/data.ts
index aa67df06c7..91a17606a9 100644
--- a/test/integrations/destinations/am/batch/data.ts
+++ b/test/integrations/destinations/am/batch/data.ts
@@ -61,7 +61,7 @@ export const data = [
endpoint: 'https://api.eu.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
destination: {
@@ -83,16 +83,24 @@ export const data = [
{
batched: false,
error: 'Both userId and deviceId cannot be undefined',
- //TODO fix this
- metadata: {
- job_id: 1,
- userId: 'u1',
- },
+ metadata: [
+ {
+ jobId: 1,
+ userId: 'u1',
+ },
+ ],
statTags: {
errorCategory: 'dataValidation',
errorType: 'instrumentation',
},
statusCode: 400,
+ destination: {
+ ID: 'a',
+ url: 'a',
+ Config: {
+ residencyServer: 'EU',
+ },
+ },
},
],
},
@@ -163,7 +171,7 @@ export const data = [
endpoint: 'https://api.eu.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
destination: {
@@ -218,7 +226,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
destination: {
@@ -273,7 +281,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
destination: {
@@ -331,7 +339,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
destination: {
@@ -389,7 +397,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
destination: {
@@ -423,7 +431,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/groupidentify',
},
metadata: {
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
destination: {
@@ -455,7 +463,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/usermap',
},
metadata: {
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
destination: {
@@ -529,7 +537,7 @@ export const data = [
},
metadata: [
{
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
],
@@ -565,7 +573,7 @@ export const data = [
},
metadata: [
{
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
],
@@ -599,7 +607,7 @@ export const data = [
},
metadata: [
{
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
],
@@ -710,19 +718,19 @@ export const data = [
},
metadata: [
{
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
{
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
{
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
{
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
],
@@ -801,7 +809,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
destination: {
@@ -854,7 +862,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
destination: {
@@ -907,7 +915,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
destination: {
@@ -963,7 +971,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
destination: {
@@ -1019,7 +1027,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
destination: {
@@ -1053,7 +1061,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/groupidentify',
},
metadata: {
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
destination: {
@@ -1085,7 +1093,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/usermap',
},
metadata: {
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
destination: {
@@ -1157,7 +1165,7 @@ export const data = [
},
metadata: [
{
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
],
@@ -1193,7 +1201,7 @@ export const data = [
},
metadata: [
{
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
],
@@ -1227,7 +1235,7 @@ export const data = [
},
metadata: [
{
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
],
@@ -1338,19 +1346,19 @@ export const data = [
},
metadata: [
{
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
{
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
{
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
{
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
],
@@ -2117,7 +2125,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
destination: {
@@ -2172,7 +2180,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
destination: {
@@ -2227,7 +2235,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
destination: {
@@ -2285,7 +2293,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
destination: {
@@ -2343,7 +2351,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
destination: {
@@ -2377,7 +2385,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/groupidentify',
},
metadata: {
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
destination: {
@@ -2409,7 +2417,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/usermap',
},
metadata: {
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
destination: {
@@ -2483,7 +2491,7 @@ export const data = [
},
metadata: [
{
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
],
@@ -2519,7 +2527,7 @@ export const data = [
},
metadata: [
{
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
],
@@ -2553,7 +2561,7 @@ export const data = [
},
metadata: [
{
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
],
@@ -2664,19 +2672,19 @@ export const data = [
},
metadata: [
{
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
{
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
{
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
{
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
],
@@ -2756,7 +2764,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
destination: {
@@ -2811,7 +2819,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
destination: {
@@ -2866,7 +2874,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
destination: {
@@ -2924,7 +2932,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
destination: {
@@ -2982,7 +2990,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/2/httpapi',
},
metadata: {
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
destination: {
@@ -3016,7 +3024,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/groupidentify',
},
metadata: {
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
destination: {
@@ -3048,7 +3056,7 @@ export const data = [
endpoint: 'https://api2.amplitude.com/usermap',
},
metadata: {
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
destination: {
@@ -3121,7 +3129,7 @@ export const data = [
},
metadata: [
{
- job_id: 1,
+ jobId: 1,
userId: 'u1',
},
],
@@ -3157,7 +3165,7 @@ export const data = [
},
metadata: [
{
- job_id: 6,
+ jobId: 6,
userId: 'u1',
},
],
@@ -3191,7 +3199,7 @@ export const data = [
},
metadata: [
{
- job_id: 7,
+ jobId: 7,
userId: 'u1',
},
],
@@ -3302,19 +3310,19 @@ export const data = [
},
metadata: [
{
- job_id: 2,
+ jobId: 2,
userId: 'u1',
},
{
- job_id: 3,
+ jobId: 3,
userId: 'u1',
},
{
- job_id: 4,
+ jobId: 4,
userId: 'u1',
},
{
- job_id: 5,
+ jobId: 5,
userId: 'u1',
},
],
diff --git a/test/integrations/destinations/am/processor/data.ts b/test/integrations/destinations/am/processor/data.ts
index f28606da0c..b645fb5ac7 100644
--- a/test/integrations/destinations/am/processor/data.ts
+++ b/test/integrations/destinations/am/processor/data.ts
@@ -11327,4 +11327,57 @@ export const data = [
},
},
},
+ {
+ name: 'am',
+ description:
+ 'Test 78 -> Page call invalid event type as page name and template is not provided',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ request_ip: '1.1.1.1',
+ type: 'page',
+ userId: '12345',
+ properties: {},
+ integrations: {
+ All: true,
+ },
+ sentAt: '2019-10-14T11:15:53.296Z',
+ },
+ destination: {
+ Config: {
+ apiKey: 'abcde',
+ useUserDefinedPageEventName: true,
+ userProvidedPageEventString: '',
+ },
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ statusCode: 400,
+ error:
+ 'Event type is missing. Please send it under `event.type`. For page/screen events, send it under `event.name`',
+ statTags: {
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ destType: 'AM',
+ module: 'destination',
+ implementation: 'native',
+ feature: 'processor',
+ },
+ },
+ ],
+ },
+ },
+ },
];
diff --git a/test/integrations/destinations/bluecore/data.ts b/test/integrations/destinations/bluecore/data.ts
new file mode 100644
index 0000000000..c2205c25a1
--- /dev/null
+++ b/test/integrations/destinations/bluecore/data.ts
@@ -0,0 +1,6 @@
+import { ecomTestData } from './ecommTestData';
+import { identifyData } from './identifyTestData';
+import { trackTestData } from './trackTestData';
+import { validationTestData } from './validationTestData';
+
+export const data = [...identifyData, ...trackTestData, ...ecomTestData, ...validationTestData];
diff --git a/test/integrations/destinations/bluecore/ecommTestData.ts b/test/integrations/destinations/bluecore/ecommTestData.ts
new file mode 100644
index 0000000000..de7584df78
--- /dev/null
+++ b/test/integrations/destinations/bluecore/ecommTestData.ts
@@ -0,0 +1,489 @@
+import { generateSimplifiedTrackPayload, transformResultBuilder } from '../../testUtils';
+
+const metadata = {
+ sourceType: '',
+ destinationType: '',
+ namespace: '',
+ destinationId: '',
+};
+
+const destination = {
+ ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
+ Name: 'BLUECORE',
+ Config: {
+ bluecoreNamespace: 'dummy_sandbox',
+ eventsMapping: [
+ {
+ from: 'ABC Searched',
+ to: 'search',
+ },
+ {
+ from: 'testPurchase',
+ to: 'purchase',
+ },
+ {
+ from: 'testboth',
+ to: 'wishlist',
+ },
+ {
+ from: 'testboth',
+ to: 'add_to_cart',
+ },
+ ],
+ },
+ Enabled: true,
+ Transformations: [],
+ DestinationDefinition: { Config: { cdkV2Enabled: true } },
+};
+
+const commonTraits = {
+ id: 'user@1',
+ age: '22',
+ anonymousId: '9c6bd77ea9da3e68',
+};
+
+const commonPropsWithProducts = {
+ property1: 'value1',
+ property2: 'value2',
+ products: [
+ {
+ product_id: '123',
+ sku: 'sku123',
+ name: 'Product 1',
+ price: 100,
+ quantity: 2,
+ },
+ {
+ product_id: '124',
+ sku: 'sku124',
+ name: 'Product 2',
+ price: 200,
+ quantity: 3,
+ },
+ ],
+};
+
+const commonPropsWithoutProducts = {
+ property1: 'value1',
+ property2: 'value2',
+ product_id: '123',
+};
+
+const commonOutputHeaders = {
+ 'Content-Type': 'application/json',
+};
+
+const eventEndPoint = 'https://api.bluecore.com/api/track/mobile/v1';
+
+export const ecomTestData = [
+ {
+ id: 'bluecore-track-test-1',
+ name: 'bluecore',
+ description:
+ 'Track event call with custom event mapped in destination config to purchase event. This will fail as order_id is not present in the payload',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'testPurchase',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ '[Bluecore] property:: order_id is required for purchase event: Workflow: procWorkflow, Step: handleTrackEvent, ChildStep: preparePayload, OriginalError: [Bluecore] property:: order_id is required for purchase event',
+ metadata,
+ statTags: {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-2',
+ name: 'bluecore',
+ description:
+ 'Track event call with custom event mapped in destination config to purchase event. This will fail as total is not present in the payload',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'testPurchase',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: { ...commonPropsWithProducts, order_id: '123' },
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ '[Bluecore] property:: total is required for purchase event: Workflow: procWorkflow, Step: handleTrackEvent, ChildStep: preparePayload, OriginalError: [Bluecore] property:: total is required for purchase event',
+ metadata,
+ statTags: {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-3',
+ name: 'bluecore',
+ description:
+ 'Track event call with products searched event not mapped in destination config. This will fail as search_query is not present in the payload',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'Products Searched',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: { ...commonPropsWithoutProducts },
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ '[Bluecore] property:: search_query is required for search event: Workflow: procWorkflow, Step: handleTrackEvent, ChildStep: preparePayload, OriginalError: [Bluecore] property:: search_query is required for search event',
+ metadata,
+ statTags: {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-4',
+ name: 'bluecore',
+ description:
+ 'Track event call with Product Viewed event not mapped in destination config. This will be sent with viewed_product name. This event without properties.products will add entire property object as products as this event type is recommended to sent with products',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'product viewed',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithoutProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'test@rudderstack.com',
+ customer: {
+ age: '22',
+ email: 'test@rudderstack.com',
+ },
+ products: [
+ {
+ id: '123',
+ property1: 'value1',
+ property2: 'value2',
+ },
+ ],
+ },
+ event: 'viewed_product',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-5',
+ name: 'bluecore',
+ description:
+ 'Track event call with custom event mapped with two standard ecomm events in destination config. Both of the two corresponding standard events will be sent ',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'testboth',
+ sentAt: '2020-08-14T05:30:30.118Z',
+ channel: 'web',
+ context: {
+ source: 'test',
+ userAgent: 'chrome',
+ traits: {
+ id: 'user@1',
+ age: '22',
+ anonymousId: '9c6bd77ea9da3e68',
+ },
+ device: {
+ advertisingId: 'abc123',
+ },
+ library: {
+ name: 'rudder-sdk-ruby-sync',
+ version: '1.0.6',
+ },
+ },
+ properties: {
+ property1: 'value1',
+ property2: 'value2',
+ product_id: '123',
+ },
+ anonymousId: 'new-id',
+ integrations: {
+ All: true,
+ },
+ },
+ metadata,
+ destination,
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'user@1',
+ customer: {
+ age: '22',
+ },
+ products: [
+ {
+ id: '123',
+ property1: 'value1',
+ property2: 'value2',
+ },
+ ],
+ },
+ event: 'wishlist',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'user@1',
+ customer: {
+ age: '22',
+ },
+ products: [
+ {
+ id: '123',
+ property1: 'value1',
+ property2: 'value2',
+ },
+ ],
+ },
+ event: 'add_to_cart',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-6',
+ name: 'bluecore',
+ description:
+ 'Track event call with Order Completed event without product array and not mapped in destination config. This will be sent with purchase name. This event without properties.products will generate error as products array is required for purchase event and ordered completed is a standard ecomm event',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'Order Completed',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithoutProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ '[Bluecore]:: products array is required for purchase event: Workflow: procWorkflow, Step: handleTrackEvent, ChildStep: preparePayload, OriginalError: [Bluecore]:: products array is required for purchase event',
+ metadata,
+ statTags: {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/bluecore/identifyTestData.ts b/test/integrations/destinations/bluecore/identifyTestData.ts
new file mode 100644
index 0000000000..660e335bc6
--- /dev/null
+++ b/test/integrations/destinations/bluecore/identifyTestData.ts
@@ -0,0 +1,381 @@
+import {
+ overrideDestination,
+ transformResultBuilder,
+ generateSimplifiedIdentifyPayload,
+} from '../../testUtils';
+
+const destination = {
+ ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
+ Name: 'BLUECORE',
+ Config: {
+ bluecoreNamespace: 'dummy_sandbox',
+ eventsMapping: [
+ {
+ from: 'ABC Searched',
+ to: 'search',
+ },
+ ],
+ },
+ Enabled: true,
+ Transformations: [],
+ DestinationDefinition: { Config: { cdkV2Enabled: true } },
+};
+
+const metadata = {
+ sourceType: '',
+ destinationType: '',
+ namespace: '',
+ destinationId: '',
+};
+
+const commonTraits = {
+ 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',
+ },
+};
+
+const contextWithExternalId = {
+ traits: {
+ ...commonTraits,
+ email: 'abc@gmail.com',
+ },
+ externalId: [{ type: 'bluecoreExternalId', id: '54321' }],
+};
+
+const commonOutputCustomerProperties = {
+ first_name: 'Test',
+ last_name: 'Rudderlabs',
+ sex: 'non-binary',
+ address: {
+ city: 'Kolkata',
+ state: 'WB',
+ zip: '700114',
+ country: 'IN',
+ },
+};
+
+const commonOutputHeaders = {
+ 'Content-Type': 'application/json',
+};
+
+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';
+
+export const identifyData = [
+ {
+ id: 'bluecore-identify-test-1',
+ name: 'bluecore',
+ description:
+ '[Success]: Identify call with all properties, that creates a customer in bluecore by default',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should containt one payload with event name as customer_patch and status code should be 200',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination,
+ metadata,
+ message: generateSimplifiedIdentifyPayload({
+ context: {
+ traits: { ...commonTraits, email: 'abc@gmail.com' },
+ },
+ anonymousId,
+ userId,
+ sentAt,
+ originalTimestamp,
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ userId: '',
+ method: 'POST',
+ endpoint: commonEndpoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'abc@gmail.com',
+ customer: { ...commonOutputCustomerProperties, email: 'abc@gmail.com' },
+ },
+ token: 'dummy_sandbox',
+ event: 'customer_patch',
+ },
+ }),
+ metadata: {
+ sourceType: '',
+ destinationType: '',
+ namespace: '',
+ destinationId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-identify-test-2',
+ name: 'bluecore',
+ description:
+ '[Success]: Identify call with all properties,along with action as identify that mandatorily needs email to link distict_id with customer in bluecore',
+ scenario: 'Business',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination,
+ metadata,
+ message: generateSimplifiedIdentifyPayload({
+ context: {
+ traits: commonTraits,
+ },
+ traits: {
+ action: 'identify',
+ },
+ anonymousId,
+ userId,
+ sentAt,
+ originalTimestamp,
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ '[Bluecore] property:: email is required for identify action: Workflow: procWorkflow, Step: prepareIdentifyPayload, ChildStep: undefined, OriginalError: [Bluecore] property:: email is required for identify action',
+ metadata: {
+ destinationId: '',
+ destinationType: '',
+ namespace: '',
+ sourceType: '',
+ },
+ statTags: {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-identify-test-3',
+ name: 'bluecore',
+ description:
+ '[Success]: Identify call with all properties,along with action as random which is not supported by bluecore for identify action',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should containt one payload with event name as identify and status code should be 200',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination,
+ metadata,
+ message: generateSimplifiedIdentifyPayload({
+ context: {
+ traits: commonTraits,
+ },
+ traits: {
+ action: 'random',
+ },
+ anonymousId,
+ userId,
+ sentAt,
+ originalTimestamp,
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ "[Bluecore] traits.action must be 'identify' for identify action: Workflow: procWorkflow, Step: prepareIdentifyPayload, ChildStep: undefined, OriginalError: [Bluecore] traits.action must be 'identify' for identify action",
+ metadata: {
+ destinationId: '',
+ destinationType: '',
+ namespace: '',
+ sourceType: '',
+ },
+ statTags: {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-identify-test-4',
+ name: 'bluecore',
+ description:
+ '[Success]: Identify call with all properties, that stitches a customer email with distinct_id in bluecore if action is identify and email is present in traits',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should containt one payload with event name as identify and status code should be 200',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination,
+ metadata,
+ message: generateSimplifiedIdentifyPayload({
+ context: {
+ traits: { ...commonTraits, email: 'abc@gmail.com' },
+ },
+ traits: {
+ action: 'identify',
+ },
+ anonymousId,
+ userId,
+ sentAt,
+ originalTimestamp,
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ userId: '',
+ method: 'POST',
+ endpoint: commonEndpoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'user@1',
+ customer: { ...commonOutputCustomerProperties, email: 'abc@gmail.com' },
+ },
+ token: 'dummy_sandbox',
+ event: 'identify',
+ },
+ }),
+ metadata: {
+ destinationId: '',
+ destinationType: '',
+ namespace: '',
+ sourceType: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-identify-test-5',
+ name: 'bluecore',
+ description:
+ '[Success]: Identify call with all properties and externalId, that creates a customer in bluecore by default, distinct_id is set to externalId value',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should containt one payload with event name as customer_patch and status code should be 200',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination,
+ metadata,
+ message: generateSimplifiedIdentifyPayload({
+ context: contextWithExternalId,
+ anonymousId,
+ userId,
+ sentAt,
+ originalTimestamp,
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ userId: '',
+ method: 'POST',
+ endpoint: commonEndpoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: '54321',
+ customer: { ...commonOutputCustomerProperties, email: 'abc@gmail.com' },
+ },
+ token: 'dummy_sandbox',
+ event: 'customer_patch',
+ },
+ }),
+ metadata: {
+ sourceType: '',
+ destinationType: '',
+ namespace: '',
+ destinationId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/bluecore/trackTestData.ts b/test/integrations/destinations/bluecore/trackTestData.ts
new file mode 100644
index 0000000000..72d48bf93d
--- /dev/null
+++ b/test/integrations/destinations/bluecore/trackTestData.ts
@@ -0,0 +1,439 @@
+import {
+ generateSimplifiedTrackPayload,
+ generateTrackPayload,
+ overrideDestination,
+ transformResultBuilder,
+} from '../../testUtils';
+
+const destination = {
+ ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
+ Name: 'BLUECORE',
+ Config: {
+ bluecoreNamespace: 'dummy_sandbox',
+ eventsMapping: [
+ {
+ from: 'ABC Searched',
+ to: 'search',
+ },
+ {
+ from: 'testPurchase',
+ to: 'purchase',
+ },
+ {
+ from: 'testboth',
+ to: 'wishlist',
+ },
+ {
+ from: 'testboth',
+ to: 'add_to_cart',
+ },
+ ],
+ },
+ Enabled: true,
+ Transformations: [],
+ DestinationDefinition: { Config: { cdkV2Enabled: true } },
+};
+
+const metadata = {
+ sourceType: '',
+ destinationType: '',
+ namespace: '',
+ destinationId: '',
+};
+
+const commonTraits = {
+ id: 'user@1',
+ age: '22',
+ anonymousId: '9c6bd77ea9da3e68',
+};
+
+const contextWithExternalId = {
+ traits: {
+ ...commonTraits,
+ email: 'abc@gmail.com',
+ },
+ externalId: [{ type: 'bluecoreExternalId', id: '54321' }],
+};
+
+const commonPropsWithProducts = {
+ property1: 'value1',
+ property2: 'value2',
+ products: [
+ {
+ product_id: '123',
+ sku: 'sku123',
+ name: 'Product 1',
+ price: 100,
+ quantity: 2,
+ },
+ {
+ product_id: '124',
+ sku: 'sku124',
+ name: 'Product 2',
+ price: 200,
+ quantity: 3,
+ },
+ ],
+};
+
+const commonPropsWithoutProducts = {
+ property1: 'value1',
+ property2: 'value2',
+ product_id: '123',
+};
+
+const commonOutputHeaders = {
+ 'Content-Type': 'application/json',
+};
+
+const eventEndPoint = 'https://api.bluecore.com/api/track/mobile/v1';
+
+export const trackTestData = [
+ {
+ id: 'bluecore-track-test-1',
+ name: 'bluecore',
+ description:
+ 'Track event call with custom event with properties not mapped in destination config. This will be sent with its original name',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should contain only event payload and status code should be 200, for the event payload should contain flattened properties in the payload',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'TestEven001',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'test@rudderstack.com',
+ customer: {
+ age: '22',
+ email: 'test@rudderstack.com',
+ },
+ products: [
+ {
+ name: 'Product 1',
+ price: 100,
+ id: '123',
+ quantity: 2,
+ },
+ {
+ name: 'Product 2',
+ price: 200,
+ id: '124',
+ quantity: 3,
+ },
+ ],
+ },
+ event: 'TestEven001',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-2',
+ name: 'bluecore',
+ description:
+ 'Track event call with custom event without properties not mapped in destination config. This will be sent with its original name',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should contain only event payload and status code should be 200. As the event paylaod does not contains products, product array will not be sent',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'TestEven001',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithoutProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'test@rudderstack.com',
+ customer: {
+ age: '22',
+ email: 'test@rudderstack.com',
+ },
+ },
+ event: 'TestEven001',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-3',
+ name: 'bluecore',
+ description:
+ 'optin event is also considered as a track event, user need to not map it from the UI , it will be sent with the same event name to bluecore',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should contain only event payload and status code should be 200, for the event payload should contain flattened properties in the payload',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'optin',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithoutProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'test@rudderstack.com',
+ customer: {
+ age: '22',
+ email: 'test@rudderstack.com',
+ },
+ },
+ event: 'optin',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-4',
+ name: 'bluecore',
+ description:
+ 'unsubscribe event is also considered as a track event, user need to not map it from the UI , it will be sent with the same event name to bluecore',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should contain only event payload and status code should be 200, for the event payload should contain flattened properties in the payload',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'unsubscribe',
+ userId: 'sajal12',
+ context: {
+ traits: {
+ ...commonTraits,
+ email: 'test@rudderstack.com',
+ phone: '9112340375',
+ },
+ },
+ properties: commonPropsWithoutProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: 'test@rudderstack.com',
+ customer: {
+ age: '22',
+ email: 'test@rudderstack.com',
+ },
+ },
+ event: 'unsubscribe',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-track-test-5',
+ name: 'bluecore',
+ description:
+ 'Track event call with with externalId. This will map externalId to distinct_id in the payload',
+ scenario: 'Business',
+ successCriteria:
+ 'Response should contain only event payload and status code should be 200, for the event payload should contain flattened properties in the payload',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: destination,
+ metadata,
+ message: generateSimplifiedTrackPayload({
+ type: 'track',
+ event: 'TestEven001',
+ userId: 'sajal12',
+ context: contextWithExternalId,
+ properties: commonPropsWithProducts,
+ anonymousId: '9c6bd77ea9da3e68',
+ originalTimestamp: '2021-01-25T15:32:56.409Z',
+ }),
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: transformResultBuilder({
+ method: 'POST',
+ endpoint: eventEndPoint,
+ headers: commonOutputHeaders,
+ JSON: {
+ properties: {
+ distinct_id: '54321',
+ customer: {
+ age: '22',
+ email: 'abc@gmail.com',
+ },
+ products: [
+ {
+ name: 'Product 1',
+ price: 100,
+ id: '123',
+ quantity: 2,
+ },
+ {
+ name: 'Product 2',
+ price: 200,
+ id: '124',
+ quantity: 3,
+ },
+ ],
+ },
+ event: 'TestEven001',
+ token: 'dummy_sandbox',
+ },
+ userId: '',
+ }),
+ metadata,
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/bluecore/validationTestData.ts b/test/integrations/destinations/bluecore/validationTestData.ts
new file mode 100644
index 0000000000..5b81f8c95a
--- /dev/null
+++ b/test/integrations/destinations/bluecore/validationTestData.ts
@@ -0,0 +1,150 @@
+const metadata = {
+ sourceType: '',
+ destinationType: '',
+ namespace: '',
+ destinationId: '',
+};
+
+const outputStatTags = {
+ destType: 'BLUECORE',
+ destinationId: '',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ feature: 'processor',
+ implementation: 'cdkV2',
+ module: 'destination',
+};
+
+export const validationTestData = [
+ {
+ id: 'bluecore-validation-test-1',
+ name: 'bluecore',
+ 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 bluecore destination and the error message should be Event type random is not supported',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: {
+ ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
+ Name: 'BLUECORE',
+ Config: {
+ bluecoreNamespace: 'dummy_sandbox',
+ eventsMapping: [
+ {
+ from: 'ABC Searched',
+ to: 'search',
+ },
+ ],
+ },
+ Enabled: true,
+ Transformations: [],
+ DestinationDefinition: { Config: { cdkV2Enabled: true } },
+ },
+ metadata,
+ 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,
+ statTags: outputStatTags,
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ id: 'bluecore-validation-test-1',
+ name: 'bluecore',
+ description: '[Error]: Check for not finding bluecoreNamespace',
+ scenario: 'Framework',
+ successCriteria:
+ 'Response should contain error message and status code should be 400, as bluecoreNamespace is not found in the destination config',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: {
+ ID: '1pYpzzvcn7AQ2W9GGIAZSsN6Mfq',
+ Name: 'BLUECORE',
+ Config: {
+ eventsMapping: [
+ {
+ from: 'ABC Searched',
+ to: 'search',
+ },
+ ],
+ },
+ Enabled: true,
+ Transformations: [],
+ DestinationDefinition: { Config: { cdkV2Enabled: true } },
+ },
+ metadata: metadata,
+ 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,
+ statTags: outputStatTags,
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/braze/dataDelivery/business.ts b/test/integrations/destinations/braze/dataDelivery/business.ts
new file mode 100644
index 0000000000..4997c5ffae
--- /dev/null
+++ b/test/integrations/destinations/braze/dataDelivery/business.ts
@@ -0,0 +1,377 @@
+import { ProxyMetdata } from '../../../../../src/types';
+import { ProxyV1TestData } from '../../../testTypes';
+import { generateMetadata, generateProxyV1Payload } from '../../../testUtils';
+
+const BRAZE_USERS_TRACK_ENDPOINT = 'https://rest.iad-03.braze.com/users/track';
+
+const partner = 'RudderStack';
+
+const headers = {
+ Accept: 'application/json',
+ Authorization: 'Bearer api_key',
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'RudderLabs',
+};
+
+const BrazeEvent1 = {
+ name: 'Product List Viewed',
+ time: '2023-11-30T21:48:45.634Z',
+ properties: {
+ products: [
+ {
+ sku: '23-04-52-62-01-18',
+ name: 'Broman Hoodie',
+ price: '97.99',
+ variant: [
+ {
+ id: 39653520310368,
+ sku: '23-04-52-62-01-18',
+ grams: 0,
+ price: '97.99',
+ title: '(SM)',
+ weight: 0,
+ option1: '(SM)',
+ taxable: true,
+ position: 1,
+ tax_code: '',
+ created_at: '2023-05-18T12:56:22-06:00',
+ product_id: 6660780884064,
+ updated_at: '2023-11-30T15:48:43-06:00',
+ weight_unit: 'kg',
+ quantity_rule: {
+ min: 1,
+ increment: 1,
+ },
+ compare_at_price: '139.99',
+ inventory_policy: 'deny',
+ requires_shipping: true,
+ inventory_quantity: 8,
+ fulfillment_service: 'manual',
+ inventory_management: 'shopify',
+ quantity_price_breaks: [],
+ old_inventory_quantity: 8,
+ },
+ ],
+ category: '62 OTHER/RETRO',
+ currency: 'CAD',
+ product_id: 6660780884064,
+ },
+ {
+ sku: '23-04-08-61-01-18',
+ name: 'Kipling Camo Hoodie',
+ price: '69.99',
+ variant: [
+ {
+ id: 39672628740192,
+ sku: '23-04-08-61-01-18',
+ grams: 0,
+ price: '69.99',
+ title: '(SM)',
+ weight: 0,
+ option1: '(SM)',
+ taxable: true,
+ position: 1,
+ tax_code: '',
+ created_at: '2023-06-28T12:52:56-06:00',
+ product_id: 6666835853408,
+ updated_at: '2023-11-30T15:48:43-06:00',
+ weight_unit: 'kg',
+ quantity_rule: {
+ min: 1,
+ increment: 1,
+ },
+ compare_at_price: '99.99',
+ inventory_policy: 'deny',
+ requires_shipping: true,
+ inventory_quantity: 8,
+ fulfillment_service: 'manual',
+ inventory_management: 'shopify',
+ quantity_price_breaks: [],
+ old_inventory_quantity: 8,
+ },
+ ],
+ category: 'Misc',
+ currency: 'CAD',
+ product_id: 6666835853408,
+ },
+ ],
+ },
+ _update_existing_only: false,
+ user_alias: {
+ alias_name: 'ab7de609-9bec-8e1c-42cd-084a1cd93a4e',
+ alias_label: 'rudder_id',
+ },
+};
+
+const BrazeEvent2 = {
+ name: 'Add to Cart',
+ time: '2020-01-24T11:59:02.403+05:30',
+ properties: {
+ revenue: 50,
+ },
+ external_id: 'mickeyMouse',
+};
+
+const BrazePurchaseEvent = {
+ product_id: '507f1f77bcf86cd799439011',
+ price: 0,
+ currency: 'USD',
+ quantity: 1,
+ time: '2020-01-24T11:59:02.402+05:30',
+ _update_existing_only: false,
+ user_alias: {
+ alias_name: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca',
+ alias_label: 'rudder_id',
+ },
+};
+
+const metadataArray = [generateMetadata(1), generateMetadata(2), generateMetadata(3)];
+
+const errorMessages = {
+ message_1: '{"events_processed":2,"purchases_processed":1,"message":"success"}',
+ message_2:
+ '{"events_processed":1,"message":"success","errors":[{"type":"\'external_id\', \'braze_id\', \'user_alias\', \'email\' or \'phone\' is required","input_array":"events","index":1},{"type":"\'quantity\' is not valid","input_array":"purchases","index":0}]}',
+ message_3:
+ '{"message":"Valid data must be provided in the \'attributes\', \'events\', or \'purchases\' fields.","errors":[{"type":"\'external_id\', \'braze_id\', \'user_alias\', \'email\' or \'phone\' is required","input_array":"events","index":0},{"type":"\'external_id\', \'braze_id\', \'user_alias\', \'email\' or \'phone\' is required","input_array":"events","index":1},{"type":"\'quantity\' is not valid","input_array":"purchases","index":0}]}',
+};
+
+const expectedStatTags = {
+ errorCategory: 'network',
+ errorType: 'aborted',
+ destType: 'BRAZE',
+ module: 'destination',
+ implementation: 'native',
+ feature: 'dataDelivery',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+};
+
+export const testScenariosForV1API: ProxyV1TestData[] = [
+ {
+ id: 'braze_v1_scenario_1',
+ name: 'braze',
+ description:
+ '[Proxy v1 API] :: Test for a valid request - 2 events and 1 purchase event are sent 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(
+ {
+ JSON: {
+ partner,
+ events: [BrazeEvent1, BrazeEvent2],
+ purchases: [BrazePurchaseEvent],
+ },
+ headers,
+ endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/valid_scenario1`,
+ },
+ metadataArray,
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: errorMessages.message_1,
+ statusCode: 200,
+ metadata: generateMetadata(1),
+ },
+ {
+ error: errorMessages.message_1,
+ statusCode: 200,
+ metadata: generateMetadata(2),
+ },
+ {
+ error: errorMessages.message_1,
+ statusCode: 200,
+ metadata: generateMetadata(3),
+ },
+ ],
+ status: 200,
+ message: 'Request for braze Processed Successfully',
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_scenario_2',
+ name: 'braze',
+ description:
+ '[Proxy v1 API] :: Test for a invalid request - 2 events and 1 purchase event are sent where the destination responds with 200 with error for a one of the event and the purchase event',
+ successCriteria: 'Should return 200 with error for one of the event and the purchase event',
+ scenario: 'Business',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ partner,
+ events: [{ ...BrazeEvent1, user_alias: undefined }, BrazeEvent2], // modifying first event to be invalid
+ purchases: [{ ...BrazePurchaseEvent, quantity: 'invalid quantity' }], // modifying purchase event to be invalid
+ },
+ headers,
+ endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario1`,
+ },
+ metadataArray,
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: errorMessages.message_2,
+ statusCode: 200,
+ metadata: generateMetadata(1),
+ },
+ {
+ error: errorMessages.message_2,
+ statusCode: 200,
+ metadata: generateMetadata(2),
+ },
+ {
+ error: errorMessages.message_2,
+ statusCode: 200,
+ metadata: generateMetadata(3),
+ },
+ ],
+ status: 200,
+ message: 'Request for braze Processed Successfully',
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_scenario_3',
+ name: 'braze',
+ description: '[Proxy v1 API] :: Test for an invalid request - all the payloads are invalid',
+ successCriteria: 'Should return 400 with error for all the payloads',
+ scenario: 'Business',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ partner,
+ events: [
+ { ...BrazeEvent1, user_alias: undefined },
+ { ...BrazeEvent2, external_id: undefined },
+ ], // modifying first event to be invalid
+ purchases: [{ ...BrazePurchaseEvent, quantity: 'invalid quantity' }], // modifying purchase event to be invalid
+ },
+ headers,
+ endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario2`,
+ },
+ metadataArray,
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: errorMessages.message_3,
+ statusCode: 400,
+ metadata: generateMetadata(1),
+ },
+ {
+ error: errorMessages.message_3,
+ statusCode: 400,
+ metadata: generateMetadata(2),
+ },
+ {
+ error: errorMessages.message_3,
+ statusCode: 400,
+ metadata: generateMetadata(3),
+ },
+ ],
+ statTags: expectedStatTags,
+ message: 'Request failed for braze with status: 400',
+ status: 400,
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_scenario_4',
+ name: 'braze',
+ description: '[Proxy v1 API] :: Test for invalid auth scneario',
+ successCriteria: 'Should return 400 for all the payloads',
+ scenario: 'Business',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ partner,
+ events: [BrazeEvent1, BrazeEvent2],
+ purchases: [BrazePurchaseEvent],
+ },
+ headers,
+ endpoint: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario3`,
+ },
+ metadataArray,
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: '{"message":"Invalid API Key"}',
+ statusCode: 401,
+ metadata: generateMetadata(1),
+ },
+ {
+ error: '{"message":"Invalid API Key"}',
+ statusCode: 401,
+ metadata: generateMetadata(2),
+ },
+ {
+ error: '{"message":"Invalid API Key"}',
+ statusCode: 401,
+ metadata: generateMetadata(3),
+ },
+ ],
+ statTags: expectedStatTags,
+ message: 'Request failed for braze with status: 401',
+ status: 401,
+ },
+ },
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/braze/dataDelivery/data.ts b/test/integrations/destinations/braze/dataDelivery/data.ts
index 3c1a97811e..2596a4b959 100644
--- a/test/integrations/destinations/braze/dataDelivery/data.ts
+++ b/test/integrations/destinations/braze/dataDelivery/data.ts
@@ -1,6 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
+import { testScenariosForV1API } from './business';
+import { otherScenariosV1 } from './other';
-export const data = [
+export const existingTestData = [
{
name: 'braze',
description: 'Test 0',
@@ -846,3 +848,5 @@ export const data = [
},
},
];
+
+export const data = [...existingTestData, ...testScenariosForV1API, ...otherScenariosV1];
diff --git a/test/integrations/destinations/braze/dataDelivery/other.ts b/test/integrations/destinations/braze/dataDelivery/other.ts
new file mode 100644
index 0000000000..9353899a65
--- /dev/null
+++ b/test/integrations/destinations/braze/dataDelivery/other.ts
@@ -0,0 +1,204 @@
+import { ProxyV1TestData } from '../../../testTypes';
+import { generateMetadata, generateProxyV1Payload } from '../../../testUtils';
+
+const expectedStatTags = {
+ errorCategory: 'network',
+ errorType: 'retryable',
+ destType: 'BRAZE',
+ module: 'destination',
+ implementation: 'native',
+ feature: 'dataDelivery',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+};
+
+export const otherScenariosV1: ProxyV1TestData[] = [
+ {
+ id: 'braze_v1_other_scenario_1',
+ name: 'braze',
+ 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',
+ }),
+ 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: expectedStatTags,
+ message: 'Request failed for braze with status: 503',
+ status: 503,
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_other_scenario_2',
+ name: 'braze',
+ 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',
+ }),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: '"Internal Server Error"',
+ statusCode: 500,
+ metadata: generateMetadata(1),
+ },
+ ],
+ statTags: expectedStatTags,
+ message: 'Request failed for braze with status: 500',
+ status: 500,
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_other_scenario_3',
+ name: 'braze',
+ 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',
+ }),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: '"Gateway Timeout"',
+ statusCode: 504,
+ metadata: generateMetadata(1),
+ },
+ ],
+ statTags: expectedStatTags,
+ message: 'Request failed for braze with status: 504',
+ status: 504,
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_other_scenario_4',
+ name: 'braze',
+ 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',
+ }),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: '""',
+ statusCode: 500,
+ metadata: generateMetadata(1),
+ },
+ ],
+ statTags: expectedStatTags,
+ message: 'Request failed for braze with status: 500',
+ status: 500,
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'braze_v1_other_scenario_5',
+ name: 'braze',
+ 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',
+ }),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ response: [
+ {
+ error: '""',
+ statusCode: 500,
+ metadata: generateMetadata(1),
+ },
+ ],
+ statTags: expectedStatTags,
+ message: 'Request failed for braze with status: 500',
+ status: 500,
+ },
+ },
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/braze/network.ts b/test/integrations/destinations/braze/network.ts
index 40d75c9d34..ae093ce1f4 100644
--- a/test/integrations/destinations/braze/network.ts
+++ b/test/integrations/destinations/braze/network.ts
@@ -524,4 +524,104 @@ const deleteNwData = [
},
},
];
-export const networkCallsData = [...deleteNwData, ...dataDeliveryMocksData];
+
+const BRAZE_USERS_TRACK_ENDPOINT = 'https://rest.iad-03.braze.com/users/track';
+
+// New Mocks for Braze
+const updatedDataDeliveryMocksData = [
+ {
+ description:
+ 'Mock response from destination depicting a valid request for 2 valid events and 1 purchase event',
+ httpReq: {
+ url: `${BRAZE_USERS_TRACK_ENDPOINT}/valid_scenario1`,
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ events_processed: 2,
+ purchases_processed: 1,
+ message: 'success',
+ },
+ status: 200,
+ },
+ },
+
+ {
+ description:
+ 'Mock response from destination depicting a request with 1 valid and 1 invalid event and 1 invalid purchase event',
+ httpReq: {
+ url: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario1`,
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ events_processed: 1,
+ message: 'success',
+ errors: [
+ {
+ type: "'external_id', 'braze_id', 'user_alias', 'email' or 'phone' is required",
+ input_array: 'events',
+ index: 1,
+ },
+ {
+ type: "'quantity' is not valid",
+ input_array: 'purchases',
+ index: 0,
+ },
+ ],
+ },
+ status: 200,
+ },
+ },
+
+ {
+ description:
+ 'Mock response from destination depicting a request with all the payloads are invalid',
+ httpReq: {
+ url: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario2`,
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ message:
+ "Valid data must be provided in the 'attributes', 'events', or 'purchases' fields.",
+ errors: [
+ {
+ type: "'external_id', 'braze_id', 'user_alias', 'email' or 'phone' is required",
+ input_array: 'events',
+ index: 0,
+ },
+ {
+ type: "'external_id', 'braze_id', 'user_alias', 'email' or 'phone' is required",
+ input_array: 'events',
+ index: 1,
+ },
+ {
+ type: "'quantity' is not valid",
+ input_array: 'purchases',
+ index: 0,
+ },
+ ],
+ },
+ status: 400,
+ },
+ },
+ {
+ description: 'Mock response from destination depicting a request with invalid credentials',
+ httpReq: {
+ url: `${BRAZE_USERS_TRACK_ENDPOINT}/invalid_scenario3`,
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ message: 'Invalid API Key',
+ },
+ status: 401,
+ },
+ },
+];
+export const networkCallsData = [
+ ...deleteNwData,
+ ...dataDeliveryMocksData,
+ ...updatedDataDeliveryMocksData,
+];
diff --git a/test/integrations/destinations/clevertap/processor/data.ts b/test/integrations/destinations/clevertap/processor/data.ts
index d79ebaa8da..6309c5ec8a 100644
--- a/test/integrations/destinations/clevertap/processor/data.ts
+++ b/test/integrations/destinations/clevertap/processor/data.ts
@@ -52,6 +52,7 @@ export const data = [
state: 'WB',
street: '',
},
+ 'category-unsubscribe': { email: ['Marketing', 'Transactional'] },
},
integrations: {
All: true,
@@ -98,10 +99,11 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
+ 'category-unsubscribe': { email: ['Marketing', 'Transactional'] },
},
identity: 'anon_id',
},
@@ -242,8 +244,8 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
},
@@ -968,8 +970,8 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
},
@@ -1111,8 +1113,8 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
},
@@ -1692,8 +1694,8 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
},
@@ -2077,8 +2079,8 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
first_name: 'John',
@@ -2230,8 +2232,8 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
first_name: 'John',
diff --git a/test/integrations/destinations/clevertap/router/data.ts b/test/integrations/destinations/clevertap/router/data.ts
index 4f1723a7da..5f25bbe83e 100644
--- a/test/integrations/destinations/clevertap/router/data.ts
+++ b/test/integrations/destinations/clevertap/router/data.ts
@@ -162,10 +162,10 @@ export const data = [
msgSms: true,
msgemail: true,
msgwhatsapp: false,
- custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
- custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
address:
'{"city":"kolkata","country":"India","postalCode":789223,"state":"WB","street":""}',
+ custom_mappings: '{"Office":"Trastkiv","Country":"Russia"}',
+ custom_tags: '["Test_User","Interested_User","DIY_Hobby"]',
},
objectId: 'anon_id',
},
diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/business.ts b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts
new file mode 100644
index 0000000000..f30bf73d7a
--- /dev/null
+++ b/test/integrations/destinations/criteo_audience/dataDelivery/business.ts
@@ -0,0 +1,255 @@
+import { ProxyV1TestData } from '../../../testTypes';
+import { generateProxyV1Payload, generateMetadata } from '../../../testUtils';
+export const headers = {
+ Authorization: 'Bearer success_access_token',
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+};
+export const params = {
+ destination: 'criteo_audience',
+};
+const method = 'PATCH';
+
+export const V1BusinessTestScenarion: ProxyV1TestData[] = [
+ {
+ id: 'criteo_audience_business_0',
+ name: 'criteo_audience',
+ description: '[Business]:: Test for gum type audience with gumCallerId with success response',
+ successCriteria: 'Should return a 200 status code with a success message',
+ scenario: 'business',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'remove',
+ identifierType: 'gum',
+ identifiers: ['sample_gum3'],
+ internalIdentifiers: false,
+ gumCallerId: '329739',
+ },
+ },
+ },
+ params,
+ headers,
+ method,
+ endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist',
+ },
+ [generateMetadata(1)],
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 200,
+ message: 'Request Processed Successfully',
+ response: [
+ {
+ error: '""',
+ metadata: generateMetadata(1),
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'criteo_audience_business_1',
+ name: 'criteo_audience',
+ scenario: 'business',
+ description: '[Business]:: Test for email type audience to add users with success response',
+ successCriteria: 'Should return a 200 status code with a success message',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ method: 'POST',
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'email',
+ internalIdentifiers: false,
+ identifiers: [
+ 'alex@email.com',
+ 'amy@email.com',
+ 'van@email.com',
+ 'alex@email.com',
+ 'amy@email.com',
+ 'van@email.com',
+ ],
+ },
+ },
+ },
+ params,
+ headers,
+ method,
+ endpoint: 'https://api.criteo.com/2022-10/audiences/34894/contactlist',
+ },
+ [generateMetadata(2)],
+ ),
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 200,
+ message: 'Request Processed Successfully',
+ response: [
+ {
+ error: '""',
+ metadata: generateMetadata(2),
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'criteo_audience_business_2',
+ name: 'criteo_audience',
+ scenario: 'business',
+ description: '[Business]:: Test for mobile type audience to remove users with success response',
+ successCriteria: 'Should return a 200 status code with a success message',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ method: 'POST',
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'remove',
+ identifierType: 'madid',
+ internalIdentifiers: false,
+ identifiers: [
+ 'sample_madid',
+ 'sample_madid_1',
+ 'sample_madid_2',
+ 'sample_madid_10',
+ 'sample_madid_13',
+ 'sample_madid_11',
+ 'sample_madid_12',
+ ],
+ },
+ },
+ },
+ params,
+ headers,
+ method,
+ endpoint: 'https://api.criteo.com/2022-10/audiences/34893/contactlist',
+ },
+ [generateMetadata(3)],
+ ),
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 200,
+ message: 'Request Processed Successfully',
+ response: [
+ {
+ error: '""',
+ metadata: generateMetadata(3),
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'criteo_audience_business_3',
+ name: 'criteo_audience',
+ scenario: 'business',
+ description: '[Business]:: Test for mobile type audience where audienceId is invalid',
+ successCriteria:
+ 'Should return a 400 status code with an error audience-invalid. It should also have the invalid audienceId in the error message as follows: "Audience is invalid"',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ method: 'POST',
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+ },
+ params,
+ headers,
+ method,
+ endpoint: 'https://api.criteo.com/2022-10/audiences/34896/contactlist',
+ },
+ [generateMetadata(4)],
+ ),
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 400,
+ message: 'AudienceId is Invalid. Please Provide Valid AudienceId',
+ response: [
+ {
+ error:
+ '{"errors":[{"traceIdentifier":"80a1a0ba3981b04da847d05700752c77","type":"authorization","code":"audience-invalid"}]}',
+ metadata: generateMetadata(4),
+ statusCode: 400,
+ },
+ ],
+ statTags: {
+ destType: 'CRITEO_AUDIENCE',
+ errorCategory: 'network',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+ errorType: 'aborted',
+ feature: 'dataDelivery',
+ implementation: 'native',
+ meta: 'instrumentation',
+ module: 'destination',
+ },
+ },
+ },
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts
index fb5b689a96..c603ef6664 100644
--- a/test/integrations/destinations/criteo_audience/dataDelivery/data.ts
+++ b/test/integrations/destinations/criteo_audience/dataDelivery/data.ts
@@ -1,4 +1,9 @@
-export const data = [
+import { generateMetadata } from '../../../testUtils';
+import { V1BusinessTestScenarion } from './business';
+import { v1OauthScenarios } from './oauth';
+import { v1OtherScenarios } from './other';
+
+const v0testCases = [
{
name: 'criteo_audience',
description: 'Test 0',
@@ -38,6 +43,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(1),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -70,7 +78,7 @@ export const data = [
version: '1',
type: 'REST',
method: 'PATCH',
- endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist',
+ endpoint: 'https://api.criteo.com/2022-10/audiences/3485/contactlist/expiredAccessToken',
headers: {
Authorization: 'Bearer success_access_token',
'Content-Type': 'application/json',
@@ -96,6 +104,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(2),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -123,8 +134,8 @@ export const data = [
statTags: {
destType: 'CRITEO_AUDIENCE',
errorCategory: 'network',
- destinationId: 'Non-determininable',
- workspaceId: 'Non-determininable',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
errorType: 'aborted',
feature: 'dataDelivery',
implementation: 'native',
@@ -147,7 +158,7 @@ export const data = [
version: '1',
type: 'REST',
method: 'PATCH',
- endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist',
+ endpoint: 'https://api.criteo.com/2022-10/audiences/34895/contactlist/invalidAccessToken',
headers: {
Authorization: 'Bearer success_access_token',
'Content-Type': 'application/json',
@@ -173,6 +184,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(3),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -200,8 +214,8 @@ export const data = [
statTags: {
destType: 'CRITEO_AUDIENCE',
errorCategory: 'network',
- destinationId: 'Non-determininable',
- workspaceId: 'Non-determininable',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
errorType: 'aborted',
feature: 'dataDelivery',
implementation: 'native',
@@ -250,6 +264,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(4),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -275,8 +292,8 @@ export const data = [
statTags: {
destType: 'CRITEO_AUDIENCE',
errorCategory: 'network',
- destinationId: 'Non-determininable',
- workspaceId: 'Non-determininable',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
errorType: 'aborted',
feature: 'dataDelivery',
implementation: 'native',
@@ -327,6 +344,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(5),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -352,8 +372,8 @@ export const data = [
statTags: {
destType: 'CRITEO_AUDIENCE',
errorCategory: 'network',
- destinationId: 'Non-determininable',
- workspaceId: 'Non-determininable',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
feature: 'dataDelivery',
implementation: 'native',
errorType: 'retryable',
@@ -403,6 +423,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(6),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -421,8 +444,8 @@ export const data = [
statTags: {
destType: 'CRITEO_AUDIENCE',
errorCategory: 'network',
- destinationId: 'Non-determininable',
- workspaceId: 'Non-determininable',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
errorType: 'throttled',
feature: 'dataDelivery',
implementation: 'native',
@@ -472,6 +495,9 @@ export const data = [
params: {
destination: 'criteo_audience',
},
+ userId: '1234',
+ metadata: generateMetadata(7),
+ destinationConfig: {},
},
method: 'POST',
},
@@ -492,8 +518,8 @@ export const data = [
statTags: {
destType: 'CRITEO_AUDIENCE',
errorCategory: 'network',
- destinationId: 'Non-determininable',
- workspaceId: 'Non-determininable',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
errorType: 'aborted',
feature: 'dataDelivery',
implementation: 'native',
@@ -506,3 +532,10 @@ export const data = [
},
},
];
+
+export const data = [
+ ...v0testCases,
+ ...V1BusinessTestScenarion,
+ ...v1OauthScenarios,
+ ...v1OtherScenarios,
+];
diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts
new file mode 100644
index 0000000000..982397f7c3
--- /dev/null
+++ b/test/integrations/destinations/criteo_audience/dataDelivery/oauth.ts
@@ -0,0 +1,133 @@
+import { params, headers } from './business';
+import { generateProxyV1Payload, generateMetadata } from '../../../testUtils';
+
+const commonStatTags = {
+ destType: 'CRITEO_AUDIENCE',
+ errorCategory: 'network',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+ errorType: 'aborted',
+ feature: 'dataDelivery',
+ implementation: 'native',
+ module: 'destination',
+};
+
+export const v1OauthScenarios = [
+ {
+ id: 'criteo_audience_oauth_0',
+ name: 'criteo_audience',
+ description: '[OAUTH]:: Test expired access token',
+ successCriteria: 'Should return a 401 status code with authErrorCategory as REFRESH_TOKEN',
+ scenario: 'oauth',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+ },
+ params,
+ headers,
+ method: 'PATCH',
+ endpoint:
+ 'https://api.criteo.com/2022-10/audiences/3485/contactlist/expiredAccessToken',
+ },
+ [generateMetadata(1)],
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 401,
+ body: {
+ output: {
+ status: 401,
+ authErrorCategory: 'REFRESH_TOKEN',
+ response: [
+ {
+ error:
+ 'The authorization token has expired during criteo_audience response transformation',
+ metadata: generateMetadata(1),
+ statusCode: 401,
+ },
+ ],
+ message:
+ 'The authorization token has expired during criteo_audience response transformation',
+ statTags: commonStatTags,
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'criteo_audience_oauth_1',
+ name: 'criteo_audience',
+ description: '[OAUTH]:: Test invalid access token',
+ successCriteria:
+ 'We should get a 401 status code with errorCode authorization-token-invalid. As we need to refresh the token for these conditions, authErrorCategory should be REFRESH_TOKEN',
+ scenario: 'oauth',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+ },
+ params,
+ headers,
+ method: 'PATCH',
+ endpoint:
+ 'https://api.criteo.com/2022-10/audiences/34895/contactlist/invalidAccessToken',
+ },
+ [generateMetadata(2)],
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 401,
+ body: {
+ output: {
+ status: 401,
+ authErrorCategory: 'REFRESH_TOKEN',
+ response: [
+ {
+ error:
+ 'The authorization header is invalid during criteo_audience response transformation',
+ metadata: generateMetadata(2),
+ statusCode: 401,
+ },
+ ],
+ statTags: commonStatTags,
+ message:
+ 'The authorization header is invalid during criteo_audience response transformation',
+ },
+ },
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/criteo_audience/dataDelivery/other.ts b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts
new file mode 100644
index 0000000000..f3a0688f88
--- /dev/null
+++ b/test/integrations/destinations/criteo_audience/dataDelivery/other.ts
@@ -0,0 +1,196 @@
+import { params, headers } from './business';
+import { generateProxyV1Payload, generateMetadata } from '../../../testUtils';
+
+export const v1OtherScenarios = [
+ {
+ id: 'criteo_audience_other_0',
+ name: 'criteo_audience',
+ description: '[Other]:: Test for checking service unavailable scenario',
+ successCriteria: 'Should return a 500 status code with',
+ scenario: 'other',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ headers,
+ params,
+ method: 'PATCH',
+ endpoint: 'https://random_test_url/test_for_internal_server_error',
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+ },
+ },
+ [generateMetadata(1)],
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 500,
+ response: [
+ {
+ error: '""',
+ metadata: generateMetadata(1),
+ statusCode: 500,
+ },
+ ],
+ message: 'Request Failed: during criteo_audience response transformation (Retryable)',
+ statTags: {
+ destType: 'CRITEO_AUDIENCE',
+ errorCategory: 'network',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+ feature: 'dataDelivery',
+ implementation: 'native',
+ errorType: 'retryable',
+ module: 'destination',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'criteo_audience_other_1',
+ name: 'criteo_audience',
+ description: '[Other]:: Test for checking throttling scenario',
+ successCriteria: 'Should return a 429 status code',
+ scenario: 'other',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ headers,
+ params,
+ method: 'PATCH',
+ endpoint: 'https://random_test_url/test_for_too_many_requests',
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+ },
+ },
+ [generateMetadata(2)],
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 429,
+ response: [
+ {
+ error: '{}',
+ metadata: generateMetadata(2),
+ statusCode: 429,
+ },
+ ],
+ message:
+ 'Request Failed: during criteo_audience response transformation - due to Request Limit exceeded, (Throttled)',
+ statTags: {
+ destType: 'CRITEO_AUDIENCE',
+ errorCategory: 'network',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+ feature: 'dataDelivery',
+ implementation: 'native',
+ errorType: 'throttled',
+ module: 'destination',
+ },
+ },
+ },
+ },
+ },
+ },
+ {
+ id: 'criteo_audience_other_2',
+ name: 'criteo_audience',
+ description: '[Other]:: Test for checking unknown error scenario',
+ successCriteria: 'Should return a 410 status code and abort the event',
+ scenario: 'other',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v1',
+ input: {
+ request: {
+ body: generateProxyV1Payload(
+ {
+ headers,
+ params,
+ method: 'PATCH',
+ JSON: {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+ },
+ endpoint: 'https://api.criteo.com/2022-10/audiences/34899/contactlist',
+ },
+ [generateMetadata(3)],
+ ),
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ status: 400,
+ response: [
+ {
+ error: '{"message":"unknown error"}',
+ metadata: generateMetadata(3),
+ statusCode: 400,
+ },
+ ],
+ message:
+ 'Request Failed: during criteo_audience response transformation with status "410" due to "{"message":"unknown error"}", (Aborted) ',
+ statTags: {
+ destType: 'CRITEO_AUDIENCE',
+ errorCategory: 'network',
+ destinationId: 'default-destinationId',
+ workspaceId: 'default-workspaceId',
+ errorType: 'aborted',
+ feature: 'dataDelivery',
+ implementation: 'native',
+ module: 'destination',
+ },
+ },
+ },
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/criteo_audience/network.ts b/test/integrations/destinations/criteo_audience/network.ts
index 959e8a2112..7ccf649e2a 100644
--- a/test/integrations/destinations/criteo_audience/network.ts
+++ b/test/integrations/destinations/criteo_audience/network.ts
@@ -1,3 +1,23 @@
+const headers = {
+ Authorization: 'Bearer success_access_token',
+ 'Content-Type': 'application/json',
+ Accept: 'application/json',
+ 'User-Agent': 'RudderLabs',
+};
+const params = { destination: 'criteo_audience' };
+const method = 'PATCH';
+const commonData = {
+ data: {
+ type: 'ContactlistAmendment',
+ attributes: {
+ operation: 'add',
+ identifierType: 'madid',
+ identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ internalIdentifiers: false,
+ },
+ },
+};
+
export const networkCallsData = [
{
httpReq: {
@@ -14,117 +34,74 @@ export const networkCallsData = [
},
},
},
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
+ params,
+ headers,
+ method,
},
httpRes: { status: 200 },
},
{
httpReq: {
- url: 'https://api.criteo.com/2022-10/audiences/3485/contactlist',
+ url: 'https://api.criteo.com/2022-10/audiences/34894/contactlist',
data: {
data: {
type: 'ContactlistAmendment',
attributes: {
operation: 'add',
- identifierType: 'madid',
- identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
+ identifierType: 'email',
internalIdentifiers: false,
+ identifiers: [
+ 'alex@email.com',
+ 'amy@email.com',
+ 'van@email.com',
+ 'alex@email.com',
+ 'amy@email.com',
+ 'van@email.com',
+ ],
},
},
},
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
- },
- httpRes: {
- code: '400',
- data: {
- errors: [
- {
- traceIdentifier: '80a1a0ba3981b04da847d05700752c77',
- type: 'authorization',
- code: 'authorization-token-expired',
- instance: '/2022-10/audiences/123/contactlist',
- title: 'The authorization token has expired',
- },
- ],
- },
- status: 401,
+ params,
+ headers,
+ method,
},
+ httpRes: { status: 200 },
},
{
httpReq: {
- url: 'https://api.criteo.com/2022-10/audiences/34895/contactlist',
+ url: 'https://api.criteo.com/2022-10/audiences/34893/contactlist',
data: {
data: {
type: 'ContactlistAmendment',
attributes: {
- operation: 'add',
+ operation: 'remove',
identifierType: 'madid',
- identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
internalIdentifiers: false,
+ identifiers: [
+ 'sample_madid',
+ 'sample_madid_1',
+ 'sample_madid_2',
+ 'sample_madid_10',
+ 'sample_madid_13',
+ 'sample_madid_11',
+ 'sample_madid_12',
+ ],
},
},
},
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
- },
- httpRes: {
- code: '400',
- data: {
- errors: [
- {
- traceIdentifier: '80a1a0ba3981b04da847d05700752c77',
- type: 'authorization',
- code: 'authorization-token-invalid',
- instance: '/2022-10/audiences/123/contactlist',
- title: 'The authorization header is invalid',
- },
- ],
- },
- status: 401,
+ params,
+ headers,
+ method,
},
+ httpRes: { status: 200 },
},
{
httpReq: {
url: 'https://api.criteo.com/2022-10/audiences/34896/contactlist',
- data: {
- data: {
- type: 'ContactlistAmendment',
- attributes: {
- operation: 'add',
- identifierType: 'madid',
- identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
- internalIdentifiers: false,
- },
- },
- },
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
+ data: commonData,
+ params,
+ headers,
+ method,
},
httpRes: {
code: '400',
@@ -143,25 +120,10 @@ export const networkCallsData = [
{
httpReq: {
url: 'https://api.criteo.com/2022-10/audiences/34897/contactlist',
- data: {
- data: {
- type: 'ContactlistAmendment',
- attributes: {
- operation: 'add',
- identifierType: 'madid',
- identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
- internalIdentifiers: false,
- },
- },
- },
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
+ data: commonData,
+ params,
+ headers,
+ method,
},
httpRes: {
code: '500',
@@ -180,50 +142,20 @@ export const networkCallsData = [
{
httpReq: {
url: 'https://api.criteo.com/2022-10/audiences/34898/contactlist',
- data: {
- data: {
- type: 'ContactlistAmendment',
- attributes: {
- operation: 'add',
- identifierType: 'madid',
- identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
- internalIdentifiers: false,
- },
- },
- },
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
+ data: commonData,
+ params,
+ headers,
+ method,
},
httpRes: { code: '429', data: {}, status: 429 },
},
{
httpReq: {
url: 'https://api.criteo.com/2022-10/audiences/34899/contactlist',
- data: {
- data: {
- type: 'ContactlistAmendment',
- attributes: {
- operation: 'add',
- identifierType: 'madid',
- identifiers: ['sample_madid', 'sample_madid_1', 'sample_madid_2'],
- internalIdentifiers: false,
- },
- },
- },
- params: { destination: 'criteo_audience' },
- headers: {
- Authorization: 'Bearer success_access_token',
- 'Content-Type': 'application/json',
- Accept: 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'PATCH',
+ data: commonData,
+ params,
+ headers,
+ method,
},
httpRes: { code: '400', data: { message: 'unknown error' }, status: 410 },
},
diff --git a/test/integrations/destinations/custify/deleteUsers/data.ts b/test/integrations/destinations/custify/deleteUsers/data.ts
new file mode 100644
index 0000000000..3c5a461f69
--- /dev/null
+++ b/test/integrations/destinations/custify/deleteUsers/data.ts
@@ -0,0 +1,160 @@
+const destType = 'custify';
+const commonData = {
+ name: destType,
+ feature: 'userDeletion',
+ module: 'destination',
+ version: 'v0',
+};
+
+export const data = [
+ {
+ description: 'Test 0: should fail when config is not being sent',
+ input: {
+ request: {
+ body: [
+ {
+ destType: destType.toUpperCase(),
+ userAttributes: [
+ {
+ userId: 'rudder1',
+ },
+ ],
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 400,
+ body: [
+ {
+ statusCode: 400,
+ error: 'Config for deletion not present',
+ },
+ ],
+ },
+ },
+ },
+ {
+ description: 'Test 1: should fail when apiKey is not present in config',
+ input: {
+ request: {
+ body: [
+ {
+ destType: destType.toUpperCase(),
+ userAttributes: [
+ {
+ userId: 'rudder2',
+ },
+ ],
+ config: {
+ apiToken: 'dummyApiKey',
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 400,
+ body: [
+ {
+ statusCode: 400,
+ error: 'api key for deletion not present',
+ },
+ ],
+ },
+ },
+ },
+
+ {
+ description: 'Test 2: should pass when one of the users is not present in destination',
+ input: {
+ request: {
+ body: [
+ {
+ destType: destType.toUpperCase(),
+ userAttributes: [
+ {
+ userId: 'rudder1',
+ },
+ {
+ userId: 'rudder2',
+ },
+ ],
+ config: {
+ apiKey: 'dummyApiKey',
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [{ statusCode: 200, status: 'successful' }],
+ },
+ },
+ },
+
+ {
+ description:
+ 'Test 3: should fail when one of the users is returning with 4xx(not 404) from destination',
+ input: {
+ request: {
+ body: [
+ {
+ destType: destType.toUpperCase(),
+ userAttributes: [
+ {
+ userId: 'rudder1',
+ },
+ {
+ userId: 'rudder3',
+ },
+ ],
+ config: {
+ apiKey: 'dummyApiKey',
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 400,
+ body: [{ statusCode: 400, error: '{"error":"User: rudder3 has a problem"}' }],
+ },
+ },
+ },
+
+ {
+ description:
+ 'Test 4: should fail when one of the userAttributes does not contain `userId`',
+ input: {
+ request: {
+ body: [
+ {
+ destType: destType.toUpperCase(),
+ userAttributes: [
+ {
+ userId: 'rudder1',
+ },
+ {
+ },
+ ],
+ config: {
+ apiKey: 'dummyApiKey',
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 400,
+ body: [{ statusCode: 400, error: 'User id for deletion not present' }],
+ },
+ },
+ },
+].map((props) => ({ ...commonData, ...props }));
diff --git a/test/integrations/destinations/custify/network.ts b/test/integrations/destinations/custify/network.ts
index 4af6545f9f..242f54c97b 100644
--- a/test/integrations/destinations/custify/network.ts
+++ b/test/integrations/destinations/custify/network.ts
@@ -1,36 +1,85 @@
export const networkCallsData = [
- {
- httpReq: {
- url: 'https://api.custify.com/company',
- method: 'POST',
+ {
+ httpReq: {
+ url: 'https://api.custify.com/company',
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ company_id: '6',
+ name: 'Pizzeria Presto',
+ signed_up_at: '2019-05-30T12:00:00.000Z',
+ size: 15,
+ website: 'www.pizzeriapresto.com',
+ industry: 'Restaurant',
+ plan: 'Platinum',
+ monthly_revenue: 1234567,
+ churned: false,
+ owners_csm: 'john.doe@mail.com',
+ owners_account: 'john.doe@mail.com',
+ parent_companies: [
+ {
+ id: '5ec50c9829d3c17c7cf455f2',
+ },
+ {
+ id: '5ec50c9829d3c17c7cf457f2',
+ },
+ ],
+ custom_attributes: {
+ restaurants: 5,
+ custom: 'template',
},
- httpRes: {
- data: {
- "company_id": "6",
- "name": "Pizzeria Presto",
- "signed_up_at": "2019-05-30T12:00:00.000Z",
- "size": 15,
- "website": "www.pizzeriapresto.com",
- "industry": "Restaurant",
- "plan": "Platinum",
- "monthly_revenue": 1234567,
- "churned": false,
- "owners_csm": "john.doe@mail.com",
- "owners_account": "john.doe@mail.com",
- "parent_companies": [
- {
- "id": "5ec50c9829d3c17c7cf455f2"
- },
- {
- "id": "5ec50c9829d3c17c7cf457f2"
- }
- ],
- "custom_attributes": {
- "restaurants": 5,
- "custom": "template"
- }
- },
- status: 200
- },
- }
+ },
+ status: 200,
+ },
+ },
+
+ {
+ httpReq: {
+ method: 'delete',
+ url: 'https://api.custify.com/people?user_id=rudder1',
+ headers: {
+ Authorization: 'Bearer dummyApiKey',
+ },
+ },
+ httpRes: {
+ data: {
+ msg: 'All users associated with rudder1 were successfully deleted',
+ code: 'Success',
+ params: null,
+ },
+ status: 200,
+ },
+ },
+ {
+ httpReq: {
+ method: 'delete',
+ url: 'https://api.custify.com/people?user_id=rudder2',
+ headers: {
+ Authorization: 'Bearer dummyApiKey',
+ },
+ },
+ httpRes: {
+ data: {
+ error: 'User: rudder2 not found',
+ },
+ status: 404,
+ },
+ },
+
+ {
+ httpReq: {
+ method: 'delete',
+ url: 'https://api.custify.com/people?user_id=rudder3',
+ headers: {
+ Authorization: 'Bearer dummyApiKey',
+ },
+ },
+ httpRes: {
+ data: {
+ error: 'User: rudder3 has a problem',
+ },
+ status: 400,
+ },
+ },
];
diff --git a/test/integrations/destinations/ga4/dataDelivery/data.ts b/test/integrations/destinations/ga4/dataDelivery/data.ts
new file mode 100644
index 0000000000..9ccf35e2a1
--- /dev/null
+++ b/test/integrations/destinations/ga4/dataDelivery/data.ts
@@ -0,0 +1,177 @@
+export const data = [
+ {
+ name: 'ga4',
+ description: 'Successful data delivery',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: {
+ 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: 'dummyMeasurementId',
+ },
+ body: {
+ JSON: {
+ client_id: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be',
+ timestamp_micros: 1650950229000000,
+ non_personalized_ads: true,
+ events: [
+ {
+ name: 'view_item_list',
+ params: {
+ item_list_id: 'related_products',
+ item_list_name: 'Related_products',
+ items: [
+ {
+ item_id: '507f1f77bcf86cd799439011',
+ item_name: 'Monopoly: 3rd Edition',
+ coupon: 'SUMMER_FUN',
+ item_category: 'Apparel',
+ item_brand: 'Google',
+ item_variant: 'green',
+ price: 19,
+ quantity: 2,
+ index: 1,
+ affiliation: 'Google Merchandise Store',
+ currency: 'USD',
+ discount: 2.22,
+ item_category2: 'Adult',
+ item_category3: 'Shirts',
+ item_category4: 'Crew',
+ item_category5: 'Short sleeve',
+ item_list_id: 'related_products',
+ item_list_name: 'Related Products',
+ location_id: 'L_12345',
+ },
+ ],
+ engagement_time_msec: 1,
+ },
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: {
+ output: {
+ destinationResponse: {
+ response: {
+ validationMessages: [],
+ },
+ status: 200,
+ },
+ message: '[GA4 Response Handler] - Request Processed Successfully',
+ status: 200,
+ },
+ },
+ },
+ },
+ },
+ {
+ name: 'ga4',
+ description: 'Data delivery failure',
+ feature: 'dataDelivery',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://www.google-analytics.com/debug/mp/collect',
+ headers: {
+ HOST: 'www.google-analytics.com',
+ 'Content-Type': 'application/json',
+ },
+ params: {
+ api_secret: 'dummyApiSecret',
+ measurement_id: 'dummyMeasurementId',
+ },
+ body: {
+ JSON: {
+ client_id: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be',
+ timestamp_micros: 1650950229000000,
+ non_personalized_ads: true,
+ events: [
+ {
+ name: 'view_item',
+ params: {
+ category: 'Electronics',
+ productID: 'ABC123',
+ productName: 'Example Product',
+ customer_name: 'Sample User',
+ link_imageURL: 'https://example.com/images/product.jpg',
+ customer_email: 'testrudder@gmail.com',
+ link_productURL: 'https://example.com/products/ABC123',
+ stockAvailability: true,
+ details_features_0: 'wireless charging',
+ details_features_1: 'water-resistant',
+ engagement_time_msec: 1,
+ transaction_currency: 'USD',
+ customer_loyaltyPoints: 500,
+ transaction_totalAmount: 150.99,
+ transaction_discountApplied: 20.5,
+ details_specifications_color: 'blue',
+ details_specifications_specifications_specifications_specifications_color:
+ 'blue',
+ details_specifications_specifications_specifications_specifications_weight:
+ '1.5kg',
+ },
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ },
+ },
+ output: {
+ response: {
+ status: 400,
+ body: {
+ output: {
+ destinationResponse:
+ 'The event param [string_value: "1.5kg"] has a duplicate name [details_specifications_specifications_specifications_specifications_weight].',
+ message:
+ 'Validation Server Response Handler:: Validation Error for ga4 of field path :events.params | NAME_DUPLICATED-The event param [string_value: "1.5kg"] has a duplicate name [details_specifications_specifications_specifications_specifications_weight].',
+ statTags: {
+ destType: 'GA4',
+ destinationId: 'Non-determininable',
+ errorCategory: 'network',
+ errorType: 'aborted',
+ feature: 'dataDelivery',
+ implementation: 'native',
+ module: 'destination',
+ workspaceId: 'Non-determininable',
+ },
+ status: 400,
+ },
+ },
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/ga4/network.ts b/test/integrations/destinations/ga4/network.ts
new file mode 100644
index 0000000000..e8c91ef451
--- /dev/null
+++ b/test/integrations/destinations/ga4/network.ts
@@ -0,0 +1,119 @@
+export const networkCallsData = [
+ {
+ httpReq: {
+ url: 'https://www.google-analytics.com/mp/collect',
+ headers: {
+ HOST: 'www.google-analytics.com',
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'RudderLabs',
+ },
+ params: {
+ api_secret: 'dummyApiSecret',
+ measurement_id: 'dummyMeasurementId',
+ },
+ data: {
+ client_id: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be',
+ timestamp_micros: 1650950229000000,
+ non_personalized_ads: true,
+ events: [
+ {
+ name: 'view_item_list',
+ params: {
+ item_list_id: 'related_products',
+ item_list_name: 'Related_products',
+ items: [
+ {
+ item_id: '507f1f77bcf86cd799439011',
+ item_name: 'Monopoly: 3rd Edition',
+ coupon: 'SUMMER_FUN',
+ item_category: 'Apparel',
+ item_brand: 'Google',
+ item_variant: 'green',
+ price: 19,
+ quantity: 2,
+ index: 1,
+ affiliation: 'Google Merchandise Store',
+ currency: 'USD',
+ discount: 2.22,
+ item_category2: 'Adult',
+ item_category3: 'Shirts',
+ item_category4: 'Crew',
+ item_category5: 'Short sleeve',
+ item_list_id: 'related_products',
+ item_list_name: 'Related Products',
+ location_id: 'L_12345',
+ },
+ ],
+ engagement_time_msec: 1,
+ },
+ },
+ ],
+ },
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ validationMessages: [],
+ },
+ status: 200,
+ },
+ },
+ {
+ httpReq: {
+ url: 'https://www.google-analytics.com/debug/mp/collect',
+ headers: {
+ HOST: 'www.google-analytics.com',
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'RudderLabs',
+ },
+ params: {
+ api_secret: 'dummyApiSecret',
+ measurement_id: 'dummyMeasurementId',
+ },
+ data: {
+ client_id: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be',
+ timestamp_micros: 1650950229000000,
+ non_personalized_ads: true,
+ events: [
+ {
+ name: 'view_item',
+ params: {
+ category: 'Electronics',
+ productID: 'ABC123',
+ productName: 'Example Product',
+ customer_name: 'Sample User',
+ link_imageURL: 'https://example.com/images/product.jpg',
+ customer_email: 'testrudder@gmail.com',
+ link_productURL: 'https://example.com/products/ABC123',
+ stockAvailability: true,
+ details_features_0: 'wireless charging',
+ details_features_1: 'water-resistant',
+ engagement_time_msec: 1,
+ transaction_currency: 'USD',
+ customer_loyaltyPoints: 500,
+ transaction_totalAmount: 150.99,
+ transaction_discountApplied: 20.5,
+ details_specifications_color: 'blue',
+ details_specifications_specifications_specifications_specifications_color: 'blue',
+ details_specifications_specifications_specifications_specifications_weight: '1.5kg',
+ },
+ },
+ ],
+ },
+ method: 'POST',
+ },
+ httpRes: {
+ data: {
+ validationMessages: [
+ {
+ fieldPath: 'events.params',
+ description:
+ 'The event param [string_value: "1.5kg"] has a duplicate name [details_specifications_specifications_specifications_specifications_weight].',
+ validationCode: 'NAME_DUPLICATED',
+ },
+ ],
+ },
+ status: 200,
+ },
+ },
+];
diff --git a/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts b/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts
index 1536af8187..a38980f0e9 100644
--- a/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts
+++ b/test/integrations/destinations/google_adwords_offline_conversions/router/data.ts
@@ -96,6 +96,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
+ jobId: 1,
userId: 'u1',
},
destination: {
@@ -211,6 +212,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
+ jobId: 2,
userId: 'u1',
},
destination: {
@@ -274,7 +276,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
- jobId: 1,
+ jobId: 3,
userId: 'u1',
},
destination: {
@@ -350,7 +352,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
- jobId: 2,
+ jobId: 4,
userId: 'u1',
},
destination: {
@@ -426,7 +428,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
- jobId: 3,
+ jobId: 5,
userId: 'u1',
},
destination: {
@@ -479,80 +481,130 @@ export const data = [
body: {
output: [
{
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint:
- 'https://googleads.googleapis.com/v14/customers/7693729833/offlineUserDataJobs',
- headers: {
- Authorization: 'Bearer abcd1234',
- 'Content-Type': 'application/json',
- 'developer-token': 'ijkl91011',
- },
- params: { event: 'Store sales', customerId: '7693729833' },
- body: {
- JSON: {
- event: '7693729833',
- isStoreConversion: true,
- createJobPayload: {
- job: {
- storeSalesMetadata: {
- loyaltyFraction: 1,
- transaction_upload_fraction: '1',
+ batchedRequest: [
+ {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint:
+ 'https://googleads.googleapis.com/v14/customers/7693729833/offlineUserDataJobs',
+ headers: {
+ Authorization: 'Bearer abcd1234',
+ 'Content-Type': 'application/json',
+ 'developer-token': 'ijkl91011',
+ },
+ params: { event: 'Store sales', customerId: '7693729833' },
+ body: {
+ JSON: {
+ event: '7693729833',
+ isStoreConversion: true,
+ createJobPayload: {
+ job: {
+ storeSalesMetadata: {
+ loyaltyFraction: 1,
+ transaction_upload_fraction: '1',
+ },
+ type: 'STORE_SALES_UPLOAD_FIRST_PARTY',
},
- type: 'STORE_SALES_UPLOAD_FIRST_PARTY',
},
- },
- addConversionPayload: {
- operations: [
- {
- create: {
- transaction_attribute: {
- store_attribute: { store_code: 'store code' },
- transaction_amount_micros: '100000000',
- order_id: 'order id',
- currency_code: 'INR',
- transaction_date_time: '2019-10-14 16:45:18+05:30',
- },
- userIdentifiers: [
- {
- hashedEmail:
- '6db61e6dcbcf2390e4a46af426f26a133a3bee45021422fc7ae86e9136f14110',
+ addConversionPayload: {
+ operations: [
+ {
+ create: {
+ transaction_attribute: {
+ store_attribute: { store_code: 'store code' },
+ transaction_amount_micros: '100000000',
+ order_id: 'order id',
+ currency_code: 'INR',
+ transaction_date_time: '2019-10-14 16:45:18+05:30',
},
- ],
- },
- },
- {
- create: {
- transaction_attribute: {
- store_attribute: { store_code: 'store code2' },
- transaction_amount_micros: '100000000',
- order_id: 'order id',
- currency_code: 'INR',
- transaction_date_time: '2019-10-14 16:45:18+05:30',
+ userIdentifiers: [
+ {
+ hashedEmail:
+ '6db61e6dcbcf2390e4a46af426f26a133a3bee45021422fc7ae86e9136f14110',
+ },
+ ],
},
- userIdentifiers: [
- {
- hashedEmail:
- '6db61e6dcbcf2390e4a46af426f26a133a3bee45021422fc7ae86e9136f14110',
+ },
+ {
+ create: {
+ transaction_attribute: {
+ store_attribute: { store_code: 'store code2' },
+ transaction_amount_micros: '100000000',
+ order_id: 'order id',
+ currency_code: 'INR',
+ transaction_date_time: '2019-10-14 16:45:18+05:30',
},
- ],
+ userIdentifiers: [
+ {
+ hashedEmail:
+ '6db61e6dcbcf2390e4a46af426f26a133a3bee45021422fc7ae86e9136f14110',
+ },
+ ],
+ },
},
+ ],
+ enable_partial_failure: false,
+ enable_warnings: false,
+ validate_only: true,
+ },
+ executeJobPayload: { validate_only: true },
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ },
+ {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint:
+ 'https://googleads.googleapis.com/v14/customers/7693729833:uploadCallConversions',
+ headers: {
+ Authorization: 'Bearer abcd1234',
+ 'Content-Type': 'application/json',
+ 'developer-token': 'ijkl91011',
+ },
+ params: {
+ event: 'Order Completed',
+ customerId: '7693729833',
+ customVariables: [{ from: '', to: '' }],
+ properties: {
+ loyaltyFraction: 1,
+ order_id: 'order id',
+ currency: 'INR',
+ revenue: '100',
+ store_code: 'store code2',
+ email: 'alex@example.com',
+ gclid: 'gclid',
+ product_id: '123445',
+ quantity: 123,
+ callerId: '1234',
+ callStartDateTime: '2019-10-14T11:15:18.299Z',
+ },
+ },
+ body: {
+ JSON: {
+ conversions: [
+ {
+ callerId: '1234',
+ callStartDateTime: '2019-10-14T11:15:18.299Z',
+ conversionDateTime: '2019-10-14 16:45:18+05:30',
+ conversionValue: 100,
+ currencyCode: 'INR',
},
],
- enable_partial_failure: false,
- enable_warnings: false,
- validate_only: true,
+ partialFailure: true,
},
- executeJobPayload: { validate_only: true },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
},
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
+ files: {},
},
- files: {},
- },
+ ],
metadata: [
{
secret: {
@@ -560,7 +612,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
- jobId: 1,
+ jobId: 3,
userId: 'u1',
},
{
@@ -569,7 +621,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
- jobId: 2,
+ jobId: 4,
userId: 'u1',
},
],
@@ -714,6 +766,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
+ jobId: 1,
userId: 'u1',
},
],
@@ -824,6 +877,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
+ jobId: 2,
userId: 'u1',
},
],
@@ -857,108 +911,6 @@ export const data = [
},
},
},
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint:
- 'https://googleads.googleapis.com/v14/customers/7693729833:uploadCallConversions',
- headers: {
- Authorization: 'Bearer abcd1234',
- 'Content-Type': 'application/json',
- 'developer-token': 'ijkl91011',
- },
- params: {
- event: 'Order Completed',
- customerId: '7693729833',
- customVariables: [{ from: '', to: '' }],
- properties: {
- loyaltyFraction: 1,
- order_id: 'order id',
- currency: 'INR',
- revenue: '100',
- store_code: 'store code2',
- email: 'alex@example.com',
- gclid: 'gclid',
- product_id: '123445',
- quantity: 123,
- callerId: '1234',
- callStartDateTime: '2019-10-14T11:15:18.299Z',
- },
- },
- body: {
- JSON: {
- conversions: [
- {
- callerId: '1234',
- callStartDateTime: '2019-10-14T11:15:18.299Z',
- conversionDateTime: '2019-10-14 16:45:18+05:30',
- conversionValue: 100,
- currencyCode: 'INR',
- },
- ],
- partialFailure: true,
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- secret: {
- access_token: 'abcd1234',
- refresh_token: 'efgh5678',
- developer_token: 'ijkl91011',
- },
- jobId: 2,
- userId: 'u1',
- },
- ],
- batched: false,
- statusCode: 200,
- destination: {
- Config: {
- rudderAccountId: '2Hsy2iFyoG5VLDd9wQcggHLMYFA',
- customerId: '769-372-9833',
- subAccount: false,
- UserIdentifierSource: 'FIRST_PARTY',
- conversionEnvironment: 'none',
- defaultUserIdentifier: 'email',
- hashUserIdentifier: true,
- validateOnly: true,
- eventsToOfflineConversionsTypeMapping: [
- { from: 'Data Reading Guide', to: 'click' },
- { from: 'Order Completed', to: 'store' },
- { from: 'Sign-up - click', to: 'click' },
- { from: 'Outbound click (rudderstack.com)', to: 'click' },
- { from: 'Page view', to: 'click' },
- { from: 'download', to: 'click' },
- { from: 'Product Clicked', to: 'store' },
- { from: 'Order Completed', to: 'call' },
- ],
- loginCustomerId: '4219454086',
- eventsToConversionsNamesMapping: [
- { from: 'Data Reading Guide', to: 'Data Reading Guide' },
- { from: 'Order Completed', to: 'Order Completed' },
- { from: 'Sign-up - click', to: 'Sign-up - click' },
- {
- from: 'Outbound click (rudderstack.com)',
- to: 'Outbound click (rudderstack.com)',
- },
- { from: 'Page view', to: 'Page view' },
- { from: 'Sign up completed', to: 'Sign-up - click' },
- { from: 'download', to: 'Page view' },
- { from: 'Product Clicked', to: 'Store sales' },
- ],
- authStatus: 'active',
- oneTrustCookieCategories: [],
- customVariables: [{ from: '', to: '' }],
- },
- },
- },
{
metadata: [
{
@@ -967,7 +919,7 @@ export const data = [
refresh_token: 'efgh5678',
developer_token: 'ijkl91011',
},
- jobId: 3,
+ jobId: 5,
userId: 'u1',
},
],
diff --git a/test/integrations/destinations/reddit/processor/data.ts b/test/integrations/destinations/reddit/processor/data.ts
index f3cd4ebf7b..49e0cd2baa 100644
--- a/test/integrations/destinations/reddit/processor/data.ts
+++ b/test/integrations/destinations/reddit/processor/data.ts
@@ -166,6 +166,173 @@ export const data = [
},
},
},
+ {
+ name: 'reddit',
+ description: 'Track call with order completed event with floating point values for revenue',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ context: {
+ traits: {
+ email: 'testone@gmail.com',
+ },
+ 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',
+ ip: '54.100.200.255',
+ device: {
+ advertisingId: 'asfds7fdsihf734b34j43f',
+ },
+ os: {
+ name: 'android',
+ },
+ },
+ type: 'track',
+ session_id: '16733896350494',
+ originalTimestamp: '2019-10-14T09:03:17.562Z',
+ anonymousId: '123456',
+ event: 'Order Completed',
+ userId: 'testuserId1',
+ properties: {
+ checkout_id: '12345',
+ order_id: '1234',
+ affiliation: 'Apple Store',
+ total: 20,
+ revenue: 14.985,
+ shipping: 4,
+ tax: 1,
+ discount: 1.5,
+ coupon: 'ImagePro',
+ currency: 'USD',
+ products: [
+ {
+ product_id: '123',
+ sku: 'G-32',
+ name: 'Monopoly',
+ price: 14,
+ quantity: 1,
+ category: 'Games',
+ url: 'https://www.website.com/product/path',
+ image_url: 'https://www.website.com/product/path.jpg',
+ },
+ {
+ product_id: '345',
+ sku: 'F-32',
+ name: 'UNO',
+ price: 3.45,
+ quantity: 2,
+ category: 'Games',
+ },
+ ],
+ },
+ integrations: {
+ All: true,
+ },
+ sentAt: '2019-10-14T09:03:22.563Z',
+ },
+ destination: {
+ Config: {
+ accountId: 'a2_fsddXXXfsfd',
+ hashData: true,
+ eventsMapping: [
+ {
+ from: 'Order Completed',
+ to: 'Purchase',
+ },
+ ],
+ },
+ DestinationDefinition: { Config: { cdkV2Enabled: true } },
+ },
+ metadata: {
+ destinationId: 'destId',
+ workspaceId: 'wspId',
+ secret: {
+ accessToken: 'dummyAccessToken',
+ },
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://ads-api.reddit.com/api/v2.0/conversions/events/a2_fsddXXXfsfd',
+ headers: {
+ Authorization: 'Bearer dummyAccessToken',
+ 'Content-Type': 'application/json',
+ },
+ params: {},
+ body: {
+ JSON: {
+ events: [
+ {
+ event_at: '2019-10-14T09:03:17.562Z',
+ event_type: {
+ tracking_type: 'Purchase',
+ },
+ user: {
+ aaid: 'c12d34889302d3c656b5699fa9190b51c50d6f62fce57e13bd56b503d66c487a',
+ email: 'ac144532d9e4efeab19475d9253a879173ea12a3d2238d1cb8a332a7b3a105f2',
+ external_id:
+ '7b023241a3132b792a5a33915a5afb3133cbb1e13d72879689bf6504de3b036d',
+ ip_address:
+ 'e80bd55a3834b7c2a34ade23c7ecb54d2a49838227080f50716151e765a619db',
+ user_agent:
+ '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',
+ screen_dimensions: {},
+ },
+ event_metadata: {
+ item_count: 2,
+ currency: 'USD',
+ value: 1499,
+ value_decimal: 14.99,
+ products: [
+ {
+ id: '123',
+ name: 'Monopoly',
+ category: 'Games',
+ },
+ {
+ id: '345',
+ name: 'UNO',
+ category: 'Games',
+ },
+ ],
+ },
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ destinationId: 'destId',
+ workspaceId: 'wspId',
+ secret: {
+ accessToken: 'dummyAccessToken',
+ },
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
{
name: 'reddit',
description: 'Track call with product list viewed event',
diff --git a/test/integrations/destinations/the_trade_desk/common.ts b/test/integrations/destinations/the_trade_desk/common.ts
index 8425d56431..28f46df829 100644
--- a/test/integrations/destinations/the_trade_desk/common.ts
+++ b/test/integrations/destinations/the_trade_desk/common.ts
@@ -9,14 +9,12 @@ const segmentName = 'test-segment';
const trackerId = 'test-trackerId';
const sampleDestination: Destination = {
-
Config: {
advertiserId,
advertiserSecretKey: 'test-advertiser-secret-key',
dataServer: 'apac',
ttlInDays: 30,
audienceId: segmentName,
- trackerId,
},
DestinationDefinition: {
Config: { cdkV2Enabled: true },
@@ -50,73 +48,12 @@ const sampleContext = {
sources: sampleSource,
};
-const sampleContextForConversion = {
- app: {
- build: '1.0.0',
- name: 'RudderLabs Android SDK',
- namespace: 'com.rudderlabs.javascript',
- version: '1.0.5',
- },
- device: {
- adTrackingEnabled: true,
- advertisingId: '3f034872-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,
- },
- externalId: [
- {
- type: 'daid',
- id: 'test-daid',
- },
- ],
- ip: '0.0.0.0',
- page: {
- referrer: 'https://docs.rudderstack.com/destinations/trade_desk',
- },
- library: {
- name: 'RudderLabs JavaScript SDK',
- version: '1.0.5',
- },
- locale: 'en-GB',
- os: {
- name: '',
- version: '',
- },
- screen: {
- density: 2,
- },
- userAgent:
- 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36',
-};
-
-const integrationObject = {
- All: true,
- THE_TRADE_DESK: {
- policies: ['LDU'],
- region: 'US-CA',
- privacy_settings: [
- {
- privacy_type: 'GDPR',
- is_applicable: 1,
- consent_string: 'ok',
- },
- ],
- },
-};
-
export {
destType,
destTypeInUpperCase,
advertiserId,
dataProviderId,
segmentName,
- trackerId,
sampleDestination,
sampleContext,
- sampleContextForConversion,
- integrationObject,
};
diff --git a/test/integrations/destinations/the_trade_desk/delivery/data.ts b/test/integrations/destinations/the_trade_desk/delivery/data.ts
index da8f60972e..320eb6dcfe 100644
--- a/test/integrations/destinations/the_trade_desk/delivery/data.ts
+++ b/test/integrations/destinations/the_trade_desk/delivery/data.ts
@@ -5,7 +5,6 @@ import {
dataProviderId,
segmentName,
sampleDestination,
- trackerId,
} from '../common';
beforeAll(() => {
@@ -246,169 +245,4 @@ export const data = [
},
},
},
- {
- name: destType,
- description: 'Successful delivery of realtime conversion event to Trade Desk',
- feature: 'dataDelivery',
- module: 'destination',
- version: 'v0',
- input: {
- request: {
- body: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- event_name: 'viewitem',
- value: 249.95000000000002,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 5,
- price: 49.99,
- },
- ],
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- method: 'POST',
- },
- },
- output: {
- response: {
- status: 200,
- body: {
- output: {
- destinationResponse: {
- response: {
- Message: null,
- EventResponses: [],
- },
- status: 200,
- },
- message: 'Request Processed Successfully',
- status: 200,
- },
- },
- },
- },
- },
- {
- name: destType,
- description:
- 'Error response from the Trade Desk due to invalid real time conversion event payload',
- feature: 'dataDelivery',
- module: 'destination',
- version: 'v0',
- input: {
- request: {
- body: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- event_name: 'viewitem',
- value: 249.95000000000002,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 5,
- price: 49.99,
- },
- ],
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- privacy_settings: [{}],
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- method: 'POST',
- },
- },
- output: {
- response: {
- status: 400,
- body: {
- output: {
- destinationResponse: {
- response: {
- Message: null,
- EventResponses: [
- {
- EventIndex: 0,
- EventErrors: [
- {
- Error: 'InvalidPrivacySetting',
- ErrorMessage: 'The request has an invalid privacy setting.',
- },
- ],
- EventWarnings: [],
- Successful: false,
- },
- ],
- },
- status: 400,
- },
- message:
- 'Request failed with status: 400 due to {"Message":null,"EventResponses":[{"EventIndex":0,"EventErrors":[{"Error":"InvalidPrivacySetting","ErrorMessage":"The request has an invalid privacy setting."}],"EventWarnings":[],"Successful":false}]}',
- statTags: {
- destType: destTypeInUpperCase,
- destinationId: 'Non-determininable',
- errorCategory: 'network',
- errorType: 'aborted',
- feature: 'dataDelivery',
- implementation: 'native',
- module: 'destination',
- workspaceId: 'Non-determininable',
- },
- status: 400,
- },
- },
- },
- },
- },
];
diff --git a/test/integrations/destinations/the_trade_desk/network.ts b/test/integrations/destinations/the_trade_desk/network.ts
index 5908cbf8f5..ed6bdf4c7d 100644
--- a/test/integrations/destinations/the_trade_desk/network.ts
+++ b/test/integrations/destinations/the_trade_desk/network.ts
@@ -1,4 +1,4 @@
-import { destType, advertiserId, dataProviderId, segmentName, trackerId } from './common';
+import { destType, advertiserId, dataProviderId, segmentName } from './common';
export const networkCallsData = [
{
@@ -103,107 +103,4 @@ export const networkCallsData = [
statusText: 'Ok',
},
},
- {
- httpReq: {
- url: 'https://insight.adsrvr.org/track/realtimeconversion',
- data: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- event_name: 'viewitem',
- value: 249.95000000000002,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 5,
- price: 49.99,
- },
- ],
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- },
- ],
- },
- params: { destination: destType },
- headers: {
- 'Content-Type': 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'POST',
- },
- httpRes: {
- data: {
- Message: null,
- EventResponses: [],
- },
- status: 200,
- statusText: 'OK',
- },
- },
- {
- httpReq: {
- url: 'https://insight.adsrvr.org/track/realtimeconversion',
- data: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- event_name: 'viewitem',
- value: 249.95000000000002,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 5,
- price: 49.99,
- },
- ],
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- privacy_settings: [{}],
- },
- ],
- },
- params: { destination: destType },
- headers: {
- 'Content-Type': 'application/json',
- 'User-Agent': 'RudderLabs',
- },
- method: 'POST',
- },
- httpRes: {
- data: {
- Message: null,
- EventResponses: [
- {
- EventIndex: 0,
- EventErrors: [
- {
- Error: 'InvalidPrivacySetting',
- ErrorMessage: 'The request has an invalid privacy setting.',
- },
- ],
- EventWarnings: [],
- Successful: false,
- },
- ],
- },
- status: 400,
- statusText: 'Bad Request',
- },
- },
];
diff --git a/test/integrations/destinations/the_trade_desk/router/data.ts b/test/integrations/destinations/the_trade_desk/router/data.ts
index 691ec703b9..f095f561db 100644
--- a/test/integrations/destinations/the_trade_desk/router/data.ts
+++ b/test/integrations/destinations/the_trade_desk/router/data.ts
@@ -6,11 +6,8 @@ import {
advertiserId,
dataProviderId,
segmentName,
- trackerId,
sampleDestination,
sampleContext,
- sampleContextForConversion,
- integrationObject,
} from '../common';
export const data = [
@@ -868,786 +865,6 @@ export const data = [
},
mockFns: defaultMockFns,
},
- {
- name: destType,
- description:
- 'Mapped Ecommerce events (product added, product viewed, product added to wishlist, cart viewed, checkout started, order completed)',
- feature: 'router',
- module: 'destination',
- version: 'v0',
- input: {
- request: {
- body: {
- input: [
- {
- message: {
- type: 'track',
- event: 'Product Added',
- messageId: 'messageId123',
- context: sampleContextForConversion,
- properties: {
- product_id: '622c6f5d5cf86a4c77358033',
- sku: '8472-998-0112',
- category: 'Games',
- name: 'Cones of Dunshire',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- price: 49.99,
- quantity: 5,
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- key1: 'value1',
- },
- integrations: integrationObject,
- },
- destination: overrideDestination(sampleDestination, {
- customProperties: [
- {
- rudderProperty: 'properties.key1',
- tradeDeskProperty: 'td1',
- },
- {
- rudderProperty: 'properties.key2',
- tradeDeskProperty: 'td2',
- },
- ],
- }),
- metadata: {
- jobId: 1,
- },
- },
- {
- message: {
- type: 'track',
- event: 'Product Viewed',
- properties: {
- product_id: '622c6f5d5cf86a4c77358033',
- sku: '8472-998-0112',
- category: 'Games',
- name: 'Cones of Dunshire',
- brand: 'Wyatt Games',
- variant: 'exapansion 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',
- },
- },
- destination: sampleDestination,
- metadata: {
- jobId: 2,
- },
- },
- {
- message: {
- type: 'track',
- event: 'Product Added to Wishlist',
- properties: {
- wishlist_id: '74fkdjfl0jfdkdj29j030',
- wishlist_name: 'New Games',
- product_id: '622c6f5d5cf86a4c77358033',
- sku: '8472-998-0112',
- category: 'Games',
- name: 'Cones of Dunshire',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- price: 49.99,
- quantity: 1,
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.site.com/product/path',
- image_url: 'https://www.site.com/product/path.jpg',
- },
- },
- destination: sampleDestination,
- metadata: {
- jobId: 3,
- },
- },
- {
- message: {
- type: 'track',
- event: 'Cart Viewed',
- properties: {
- cart_id: '6b2c6f5aecf86a4ae77358ae3',
- products: [
- {
- product_id: '622c6f5d5cf86a4c77358033',
- sku: '8472-998-0112',
- name: 'Cones of Dunshire',
- price: 49.99,
- position: 5,
- 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.99,
- position: 2,
- category: 'Games',
- },
- ],
- },
- },
- destination: sampleDestination,
- metadata: {
- jobId: 4,
- },
- },
- {
- message: {
- type: 'track',
- event: 'Checkout Started',
- properties: {
- order_id: '40684e8f0eaf000000000000',
- affiliation: 'Vandelay Games',
- value: 52,
- revenue: 50.0,
- shipping: 4,
- tax: 3,
- discount: 5,
- coupon: 'NEWCUST5',
- currency: 'USD',
- products: [
- {
- 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',
- },
- ],
- },
- },
- destination: sampleDestination,
- metadata: {
- jobId: 5,
- },
- },
- {
- message: {
- type: 'track',
- event: 'Order Completed',
- properties: {
- checkout_id: '70324a1f0eaf000000000000',
- order_id: '40684e8f0eaf000000000000',
- affiliation: 'Vandelay Games',
- total: 52.0,
- subtotal: 45.0,
- revenue: 50.0,
- shipping: 4.0,
- tax: 3.0,
- discount: 5.0,
- coupon: 'NEWCUST5',
- currency: 'USD',
- products: [
- {
- 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',
- },
- ],
- },
- },
- destination: sampleDestination,
- metadata: {
- jobId: 6,
- },
- },
- ],
- destType,
- },
- },
- },
- output: {
- response: {
- status: 200,
- body: {
- output: [
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- event_name: 'addtocart',
- value: 249.95000000000002,
- adid: 'test-daid',
- adid_type: 'DAID',
- client_ip: '0.0.0.0',
- referrer_url: 'https://docs.rudderstack.com/destinations/trade_desk',
- imp: 'messageId123',
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 5,
- price: 49.99,
- },
- ],
- td1: 'value1',
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- data_processing_option: {
- policies: ['LDU'],
- region: 'US-CA',
- },
- privacy_settings: [
- {
- privacy_type: 'GDPR',
- is_applicable: 1,
- consent_string: 'ok',
- },
- ],
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 1,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: overrideDestination(sampleDestination, {
- customProperties: [
- {
- rudderProperty: 'properties.key1',
- tradeDeskProperty: 'td1',
- },
- {
- rudderProperty: 'properties.key2',
- tradeDeskProperty: 'td2',
- },
- ],
- }),
- },
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- event_name: 'viewitem',
- value: 249.95000000000002,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 5,
- price: 49.99,
- },
- ],
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.webp',
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 2,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: sampleDestination,
- },
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- event_name: 'wishlistitem',
- value: 49.99,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- qty: 1,
- price: 49.99,
- },
- ],
- wishlist_id: '74fkdjfl0jfdkdj29j030',
- wishlist_name: 'New Games',
- category: 'Games',
- brand: 'Wyatt Games',
- variant: 'exapansion pack',
- coupon: 'PREORDER15',
- position: 1,
- url: 'https://www.site.com/product/path',
- image_url: 'https://www.site.com/product/path.jpg',
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 3,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: sampleDestination,
- },
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- event_name: 'viewcart',
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- name: 'Cones of Dunshire',
- price: 49.99,
- position: 5,
- category: 'Games',
- url: 'https://www.website.com/product/path',
- image_url: 'https://www.website.com/product/path.jpg',
- },
- {
- item_code: '577c6f5d5cf86a4c7735ba03',
- name: 'Five Crowns',
- price: 5.99,
- position: 2,
- category: 'Games',
- },
- ],
- cart_id: '6b2c6f5aecf86a4ae77358ae3',
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 4,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: sampleDestination,
- },
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- order_id: '40684e8f0eaf000000000000',
- event_name: 'startcheckout',
- value: 50,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- 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',
- },
- {
- item_code: '577c6f5d5cf86a4c7735ba03',
- name: 'Five Crowns',
- price: 5,
- position: 2,
- category: 'Games',
- },
- ],
- affiliation: 'Vandelay Games',
- shipping: 4,
- tax: 3,
- discount: 5,
- coupon: 'NEWCUST5',
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 5,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: sampleDestination,
- },
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- currency: 'USD',
- order_id: '40684e8f0eaf000000000000',
- event_name: 'purchase',
- value: 50,
- items: [
- {
- item_code: '622c6f5d5cf86a4c77358033',
- 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',
- },
- {
- item_code: '577c6f5d5cf86a4c7735ba03',
- name: 'Five Crowns',
- price: 5,
- position: 2,
- category: 'Games',
- },
- ],
- checkout_id: '70324a1f0eaf000000000000',
- affiliation: 'Vandelay Games',
- total: 52.0,
- subtotal: 45.0,
- shipping: 4.0,
- tax: 3.0,
- discount: 5.0,
- coupon: 'NEWCUST5',
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 6,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: sampleDestination,
- },
- ],
- },
- },
- },
- },
- {
- name: destType,
- description: 'Custom event',
- feature: 'router',
- module: 'destination',
- version: 'v0',
- input: {
- request: {
- body: {
- input: [
- {
- message: {
- type: 'track',
- event: 'custom event abc',
- properties: {
- key1: 'value1',
- value: 25,
- product_id: 'prd123',
- key2: true,
- test: 'test123',
- },
- },
- destination: overrideDestination(sampleDestination, {
- customProperties: [
- {
- rudderProperty: 'properties.key1',
- tradeDeskProperty: 'td1',
- },
- {
- rudderProperty: 'properties.key2',
- tradeDeskProperty: 'td2',
- },
- ],
- }),
- metadata: {
- jobId: 1,
- },
- },
- ],
- destType,
- },
- },
- },
- output: {
- response: {
- status: 200,
- body: {
- output: [
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- event_name: 'custom event abc',
- value: 25,
- product_id: 'prd123',
- test: 'test123',
- td1: 'value1',
- td2: true,
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 1,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: overrideDestination(sampleDestination, {
- customProperties: [
- {
- rudderProperty: 'properties.key1',
- tradeDeskProperty: 'td1',
- },
- {
- rudderProperty: 'properties.key2',
- tradeDeskProperty: 'td2',
- },
- ],
- }),
- },
- ],
- },
- },
- },
- },
- {
- name: destType,
- description: 'Mapped standard trade desk event',
- feature: 'router',
- module: 'destination',
- version: 'v0',
- input: {
- request: {
- body: {
- input: [
- {
- message: {
- type: 'track',
- event: 'custom event abc',
- properties: {
- key1: 'value1',
- value: 25,
- product_id: 'prd123',
- key2: true,
- test: 'test123',
- },
- },
- destination: overrideDestination(sampleDestination, {
- eventsMapping: [
- {
- from: 'custom event abc',
- to: 'direction',
- },
- ],
- }),
- metadata: {
- jobId: 1,
- },
- },
- ],
- destType,
- },
- },
- },
- output: {
- response: {
- status: 200,
- body: {
- output: [
- {
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- event_name: 'direction',
- value: 25,
- product_id: 'prd123',
- test: 'test123',
- key1: 'value1',
- key2: true,
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 1,
- },
- ],
- batched: false,
- statusCode: 200,
- destination: overrideDestination(sampleDestination, {
- eventsMapping: [
- {
- from: 'custom event abc',
- to: 'direction',
- },
- ],
- }),
- },
- ],
- },
- },
- },
- },
{
name: destType,
description: 'Batch call with different event types',
@@ -1674,30 +891,6 @@ export const data = [
jobId: 1,
},
},
- {
- message: {
- type: 'track',
- event: 'custom event abc',
- properties: {
- key1: 'value1',
- value: 25,
- revenue: 10,
- product_id: 'prd123',
- key2: true,
- test: 'test123',
- products: [
- {
- product_id: 'prd123',
- test: 'test',
- },
- ],
- },
- },
- destination: sampleDestination,
- metadata: {
- jobId: 2,
- },
- },
{
message: {
type: 'identify',
@@ -1711,7 +904,7 @@ export const data = [
},
destination: sampleDestination,
metadata: {
- jobId: 3,
+ jobId: 2,
},
},
],
@@ -1767,53 +960,8 @@ export const data = [
destination: sampleDestination,
},
{
- batchedRequest: {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
- headers: {},
- params: {},
- body: {
- JSON: {
- data: [
- {
- tracker_id: trackerId,
- adv: advertiserId,
- event_name: 'custom event abc',
- value: 25,
- product_id: 'prd123',
- test: 'test123',
- key1: 'value1',
- key2: true,
- revenue: 10,
- products: [
- {
- product_id: 'prd123',
- test: 'test',
- },
- ],
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- metadata: [
- {
- jobId: 2,
- },
- ],
batched: false,
- statusCode: 200,
- destination: sampleDestination,
- },
- {
- batched: false,
- metadata: [{ jobId: 3 }],
+ metadata: [{ jobId: 2 }],
statusCode: 400,
error: 'Event type identify is not supported',
statTags: {
@@ -1831,119 +979,4 @@ export const data = [
},
},
},
- {
- name: destType,
- description: 'Tracker id is not present',
- feature: 'router',
- module: 'destination',
- version: 'v0',
- input: {
- request: {
- body: {
- input: [
- {
- message: {
- type: 'record',
- action: 'insert',
- fields: {
- DAID: 'test-daid-1',
- },
- channel: 'sources',
- context: sampleContext,
- recordId: '1',
- },
- destination: overrideDestination(sampleDestination, { trackerId: '' }),
- metadata: {
- jobId: 1,
- },
- },
- {
- message: {
- type: 'track',
- event: 'custom event abc',
- properties: {
- key1: 'value1',
- value: 25,
- product_id: 'prd123',
- key2: true,
- test: 'test123',
- },
- },
- destination: overrideDestination(sampleDestination, { trackerId: '' }),
- metadata: {
- jobId: 2,
- },
- },
- ],
- destType,
- },
- method: 'POST',
- },
- },
- output: {
- response: {
- status: 200,
- body: {
- output: [
- {
- batchedRequest: [
- {
- version: '1',
- type: 'REST',
- method: 'POST',
- endpoint: 'https://sin-data.adsrvr.org/data/advertiser',
- headers: {},
- params: {},
- body: {
- JSON: {
- DataProviderId: dataProviderId,
- AdvertiserId: advertiserId,
- Items: [
- {
- DAID: 'test-daid-1',
- Data: [
- {
- Name: segmentName,
- TTLInMinutes: 43200,
- },
- ],
- },
- ],
- },
- JSON_ARRAY: {},
- XML: {},
- FORM: {},
- },
- files: {},
- },
- ],
- metadata: [
- {
- jobId: 1,
- },
- ],
- batched: true,
- statusCode: 200,
- destination: overrideDestination(sampleDestination, { trackerId: '' }),
- },
- {
- batched: false,
- metadata: [{ jobId: 2 }],
- statusCode: 400,
- error: 'Tracking Tag ID is not present. Aborting',
- statTags: {
- errorCategory: 'dataValidation',
- errorType: 'configuration',
- destType: 'THE_TRADE_DESK',
- module: 'destination',
- implementation: 'cdkV2',
- feature: 'router',
- },
- destination: overrideDestination(sampleDestination, { trackerId: '' }),
- },
- ],
- },
- },
- },
- },
];
diff --git a/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts b/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts
new file mode 100644
index 0000000000..9b79a7bcbd
--- /dev/null
+++ b/test/integrations/destinations/the_trade_desk_real_time_conversions/common.ts
@@ -0,0 +1,91 @@
+import { Destination } from '../../../../src/types';
+
+const destType = 'the_trade_desk_real_time_conversions';
+const destTypeInUpperCase = 'THE_TRADE_DESK_REAL_TIME_CONVERSIONS';
+const advertiserId = 'test-advertiser-id';
+const trackerId = 'test-trackerId';
+const sampleDestination: Destination = {
+ Config: {
+ advertiserId,
+ trackerId,
+ },
+ Enabled: true,
+ ID: '123',
+ Name: 'TRADE_DESK_REAL_TIME_CONVERSIONS',
+ WorkspaceID: 'test-workspace-id',
+ Transformations: [],
+ DestinationDefinition: {
+ ID: '123',
+ DisplayName: 'Trade Desk',
+ Name: 'TRADE_DESK',
+ Config: { cdkV2Enabled: true },
+ },
+};
+
+const sampleContextForConversion = {
+ app: {
+ build: '1.0.0',
+ name: 'RudderLabs Android SDK',
+ namespace: 'com.rudderlabs.javascript',
+ version: '1.0.5',
+ },
+ device: {
+ adTrackingEnabled: true,
+ advertisingId: '3f034872-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,
+ },
+ externalId: [
+ {
+ type: 'daid',
+ id: 'test-daid',
+ },
+ ],
+ ip: '0.0.0.0',
+ page: {
+ referrer: 'https://docs.rudderstack.com/destinations/trade_desk',
+ },
+ library: {
+ name: 'RudderLabs JavaScript SDK',
+ version: '1.0.5',
+ },
+ locale: 'en-GB',
+ os: {
+ name: '',
+ version: '',
+ },
+ screen: {
+ density: 2,
+ },
+ userAgent:
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.117 Safari/537.36',
+};
+
+const integrationObject = {
+ All: true,
+ THE_TRADE_DESK: {
+ policies: ['LDU'],
+ region: 'US-CA',
+ privacy_settings: [
+ {
+ privacy_type: 'GDPR',
+ is_applicable: 1,
+ consent_string: 'ok',
+ },
+ ],
+ },
+};
+
+export {
+ destType,
+ destTypeInUpperCase,
+ advertiserId,
+ trackerId,
+ sampleDestination,
+ sampleContextForConversion,
+ integrationObject,
+};
diff --git a/test/integrations/destinations/the_trade_desk_real_time_conversions/processor/data.ts b/test/integrations/destinations/the_trade_desk_real_time_conversions/processor/data.ts
new file mode 100644
index 0000000000..264c760088
--- /dev/null
+++ b/test/integrations/destinations/the_trade_desk_real_time_conversions/processor/data.ts
@@ -0,0 +1,984 @@
+import { overrideDestination } from '../../../testUtils';
+import {
+ destType,
+ destTypeInUpperCase,
+ advertiserId,
+ trackerId,
+ sampleDestination,
+ sampleContextForConversion,
+ integrationObject,
+} from '../common';
+
+export const data = [
+ {
+ name: destType,
+ description: 'Missing advertiser ID in the config',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'custom event abc',
+ properties: {
+ key1: 'value1',
+ value: 25,
+ product_id: 'prd123',
+ key2: true,
+ test: 'test123',
+ },
+ },
+ destination: overrideDestination(sampleDestination, { advertiserId: '' }),
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ 'Advertiser ID is not present. Aborting: Workflow: procWorkflow, Step: validateConfig, ChildStep: undefined, OriginalError: Advertiser ID is not present. Aborting',
+ statTags: {
+ destType: destTypeInUpperCase,
+ implementation: 'cdkV2',
+ feature: 'processor',
+ module: 'destination',
+ errorCategory: 'dataValidation',
+ errorType: 'configuration',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Tracker id is not present',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'custom event abc',
+ properties: {
+ key1: 'value1',
+ value: 25,
+ product_id: 'prd123',
+ key2: true,
+ test: 'test123',
+ },
+ },
+ destination: overrideDestination(sampleDestination, { trackerId: '' }),
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ method: 'POST',
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ 'Tracking Tag ID is not present. Aborting: Workflow: procWorkflow, Step: validateConfig, ChildStep: undefined, OriginalError: Tracking Tag ID is not present. Aborting',
+ statTags: {
+ destType: destTypeInUpperCase,
+ implementation: 'cdkV2',
+ feature: 'processor',
+ module: 'destination',
+ errorCategory: 'dataValidation',
+ errorType: 'configuration',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Unsupported event type',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'identify',
+ context: {
+ traits: {
+ name: 'John Doe',
+ email: 'johndoe@gmail.com',
+ age: 25,
+ },
+ },
+ },
+ destination: sampleDestination,
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ error:
+ 'Event type identify is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: Event type identify is not supported',
+ statTags: {
+ destType: destTypeInUpperCase,
+ implementation: 'cdkV2',
+ feature: 'processor',
+ module: 'destination',
+ errorCategory: 'dataValidation',
+ errorType: 'instrumentation',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 400,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Product Added',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Product Added',
+ messageId: 'messageId123',
+ context: sampleContextForConversion,
+ 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',
+ position: 1,
+ url: 'https://www.website.com/product/path',
+ image_url: 'https://www.website.com/product/path.webp',
+ key1: 'value1',
+ },
+ integrations: integrationObject,
+ },
+ destination: overrideDestination(sampleDestination, {
+ customProperties: [
+ {
+ rudderProperty: 'properties.key1',
+ tradeDeskProperty: 'td1',
+ },
+ {
+ rudderProperty: 'properties.key2',
+ tradeDeskProperty: 'td2',
+ },
+ ],
+ }),
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ event_name: 'addtocart',
+ value: 249.95000000000002,
+ adid: 'test-daid',
+ adid_type: 'DAID',
+ client_ip: '0.0.0.0',
+ referrer_url: 'https://docs.rudderstack.com/destinations/trade_desk',
+ imp: 'messageId123',
+ items: [
+ {
+ item_code: '622c6f5d5cf86a4c77358033',
+ name: 'Cones of Dunshire',
+ qty: 5,
+ price: 49.99,
+ },
+ ],
+ td1: 'value1',
+ category: 'Games',
+ brand: 'Wyatt Games',
+ variant: 'expansion pack',
+ coupon: 'PREORDER15',
+ position: 1,
+ url: 'https://www.website.com/product/path',
+ image_url: 'https://www.website.com/product/path.webp',
+ data_processing_option: {
+ policies: ['LDU'],
+ region: 'US-CA',
+ },
+ privacy_settings: [
+ {
+ privacy_type: 'GDPR',
+ is_applicable: 1,
+ consent_string: 'ok',
+ },
+ ],
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Product Viewed',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Product Viewed',
+ 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',
+ },
+ },
+ destination: sampleDestination,
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ currency: 'USD',
+ event_name: 'viewitem',
+ value: 249.95000000000002,
+ items: [
+ {
+ item_code: '622c6f5d5cf86a4c77358033',
+ name: 'Cones of Dunshire',
+ qty: 5,
+ price: 49.99,
+ },
+ ],
+ category: 'Games',
+ brand: 'Wyatt Games',
+ variant: 'expansion pack',
+ coupon: 'PREORDER15',
+ position: 1,
+ url: 'https://www.website.com/product/path',
+ image_url: 'https://www.website.com/product/path.webp',
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Product Added to Wishlist',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Product Added to Wishlist',
+ properties: {
+ wishlist_id: '74fkdjfl0jfdkdj29j030',
+ wishlist_name: 'New Games',
+ product_id: '622c6f5d5cf86a4c77358033',
+ sku: '8472-998-0112',
+ category: 'Games',
+ name: 'Cones of Dunshire',
+ brand: 'Wyatt Games',
+ variant: 'expansion pack',
+ price: 49.99,
+ quantity: 1,
+ coupon: 'PREORDER15',
+ position: 1,
+ url: 'https://www.site.com/product/path',
+ image_url: 'https://www.site.com/product/path.jpg',
+ },
+ },
+ destination: sampleDestination,
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ event_name: 'wishlistitem',
+ value: 49.99,
+ items: [
+ {
+ item_code: '622c6f5d5cf86a4c77358033',
+ name: 'Cones of Dunshire',
+ qty: 1,
+ price: 49.99,
+ },
+ ],
+ wishlist_id: '74fkdjfl0jfdkdj29j030',
+ wishlist_name: 'New Games',
+ category: 'Games',
+ brand: 'Wyatt Games',
+ variant: 'expansion pack',
+ coupon: 'PREORDER15',
+ position: 1,
+ url: 'https://www.site.com/product/path',
+ image_url: 'https://www.site.com/product/path.jpg',
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Cart Viewed',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Cart Viewed',
+ properties: {
+ cart_id: '6b2c6f5aecf86a4ae77358ae3',
+ products: [
+ {
+ product_id: '622c6f5d5cf86a4c77358033',
+ sku: '8472-998-0112',
+ name: 'Cones of Dunshire',
+ price: 49.99,
+ position: 5,
+ 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.99,
+ position: 2,
+ category: 'Games',
+ },
+ ],
+ },
+ },
+ destination: sampleDestination,
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ event_name: 'viewcart',
+ items: [
+ {
+ item_code: '622c6f5d5cf86a4c77358033',
+ name: 'Cones of Dunshire',
+ price: 49.99,
+ position: 5,
+ category: 'Games',
+ url: 'https://www.website.com/product/path',
+ image_url: 'https://www.website.com/product/path.jpg',
+ },
+ {
+ item_code: '577c6f5d5cf86a4c7735ba03',
+ name: 'Five Crowns',
+ price: 5.99,
+ position: 2,
+ category: 'Games',
+ },
+ ],
+ cart_id: '6b2c6f5aecf86a4ae77358ae3',
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Checkout Started',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Checkout Started',
+ properties: {
+ order_id: '40684e8f0eaf000000000000',
+ affiliation: 'Vandelay Games',
+ value: 52,
+ revenue: 50.0,
+ shipping: 4,
+ tax: 3,
+ discount: 5,
+ coupon: 'NEWCUST5',
+ currency: 'USD',
+ products: [
+ {
+ 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',
+ },
+ ],
+ },
+ },
+ destination: sampleDestination,
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ currency: 'USD',
+ order_id: '40684e8f0eaf000000000000',
+ event_name: 'startcheckout',
+ value: 50,
+ items: [
+ {
+ item_code: '622c6f5d5cf86a4c77358033',
+ 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',
+ },
+ {
+ item_code: '577c6f5d5cf86a4c7735ba03',
+ name: 'Five Crowns',
+ price: 5,
+ position: 2,
+ category: 'Games',
+ },
+ ],
+ affiliation: 'Vandelay Games',
+ shipping: 4,
+ tax: 3,
+ discount: 5,
+ coupon: 'NEWCUST5',
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Order Completed',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'Order Completed',
+ properties: {
+ checkout_id: '70324a1f0eaf000000000000',
+ order_id: '40684e8f0eaf000000000000',
+ affiliation: 'Vandelay Games',
+ total: 52.0,
+ subtotal: 45.0,
+ revenue: 50.0,
+ shipping: 4.0,
+ tax: 3.0,
+ discount: 5.0,
+ coupon: 'NEWCUST5',
+ currency: 'USD',
+ products: [
+ {
+ 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',
+ },
+ ],
+ },
+ },
+ destination: sampleDestination,
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ currency: 'USD',
+ order_id: '40684e8f0eaf000000000000',
+ event_name: 'purchase',
+ value: 50,
+ items: [
+ {
+ item_code: '622c6f5d5cf86a4c77358033',
+ 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',
+ },
+ {
+ item_code: '577c6f5d5cf86a4c7735ba03',
+ name: 'Five Crowns',
+ price: 5,
+ position: 2,
+ category: 'Games',
+ },
+ ],
+ checkout_id: '70324a1f0eaf000000000000',
+ affiliation: 'Vandelay Games',
+ total: 52.0,
+ subtotal: 45.0,
+ shipping: 4.0,
+ tax: 3.0,
+ discount: 5.0,
+ coupon: 'NEWCUST5',
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Custom event',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'custom event abc',
+ properties: {
+ key1: 'value1',
+ value: 25,
+ product_id: 'prd123',
+ key2: true,
+ test: 'test123',
+ },
+ },
+ destination: overrideDestination(sampleDestination, {
+ customProperties: [
+ {
+ rudderProperty: 'properties.key1',
+ tradeDeskProperty: 'td1',
+ },
+ {
+ rudderProperty: 'properties.key2',
+ tradeDeskProperty: 'td2',
+ },
+ ],
+ }),
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ event_name: 'custom event abc',
+ value: 25,
+ product_id: 'prd123',
+ test: 'test123',
+ td1: 'value1',
+ td2: true,
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: destType,
+ description: 'Mapped standard trade desk event',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ type: 'track',
+ event: 'custom event abc',
+ properties: {
+ key1: 'value1',
+ value: 25,
+ product_id: 'prd123',
+ key2: true,
+ test: 'test123',
+ },
+ },
+ destination: overrideDestination(sampleDestination, {
+ eventsMapping: [
+ {
+ from: 'custom event abc',
+ to: 'direction',
+ },
+ ],
+ }),
+ metadata: {
+ jobId: 1,
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://insight.adsrvr.org/track/realtimeconversion',
+ headers: { 'Content-Type': 'application/json' },
+ params: {},
+ body: {
+ JSON: {
+ data: [
+ {
+ tracker_id: trackerId,
+ adv: advertiserId,
+ event_name: 'direction',
+ value: 25,
+ product_id: 'prd123',
+ test: 'test123',
+ key1: 'value1',
+ key2: true,
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ metadata: {
+ jobId: 1,
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+];
diff --git a/test/integrations/destinations/tiktok_ads/processor/data.ts b/test/integrations/destinations/tiktok_ads/processor/data.ts
index 9d7c3a8d10..3b68426fbf 100644
--- a/test/integrations/destinations/tiktok_ads/processor/data.ts
+++ b/test/integrations/destinations/tiktok_ads/processor/data.ts
@@ -6870,4 +6870,107 @@ export const data = [
},
},
},
+ {
+ name: 'tiktok_ads',
+ description: 'Test 46 -> V2 -> Event with no properties',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ message: {
+ anonymousId: '21e13f4bc7ceddad',
+ channel: 'web',
+ context: {
+ traits: {
+ email: 'dd6ff77f54e2106661089bae4d40cdb600979bf7edc9eb65c0942ba55c7c2d7f',
+ },
+ userAgent:
+ 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion',
+ ip: '13.57.97.131',
+ locale: 'en-US',
+ externalId: [
+ {
+ type: 'tiktokExternalId',
+ id: 'f0e388f53921a51f0bb0fc8a2944109ec188b59172935d8f23020b1614cc44bc',
+ },
+ ],
+ },
+ messageId: '84e26acc-56a5-4835-8233-591137fca468',
+ session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22',
+ originalTimestamp: '2019-10-14T09:03:17.562Z',
+ timestamp: '2020-09-17T19:49:27Z',
+ type: 'track',
+ event: 'customEvent',
+ integrations: {
+ All: true,
+ },
+ sentAt: '2019-10-14T09:03:22.563Z',
+ },
+ destination: {
+ Config: {
+ version: 'v2',
+ accessToken: 'dummyAccessToken',
+ pixelCode: '{{PIXEL-CODE}}',
+ hashUserProperties: false,
+ sendCustomEvents: true,
+ },
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://business-api.tiktok.com/open_api/v1.3/event/track/',
+ headers: {
+ 'Access-Token': 'dummyAccessToken',
+ 'Content-Type': 'application/json',
+ },
+ params: {},
+ body: {
+ JSON: {
+ event_source: 'web',
+ event_source_id: '{{PIXEL-CODE}}',
+ partner_name: 'RudderStack',
+ data: [
+ {
+ event: 'customEvent',
+ event_id: '84e26acc-56a5-4835-8233-591137fca468',
+ event_time: 1600372167,
+ properties: { content_type: 'product' },
+ user: {
+ locale: 'en-US',
+ email: 'dd6ff77f54e2106661089bae4d40cdb600979bf7edc9eb65c0942ba55c7c2d7f',
+ external_id:
+ 'f0e388f53921a51f0bb0fc8a2944109ec188b59172935d8f23020b1614cc44bc',
+ ip: '13.57.97.131',
+ user_agent:
+ 'Mozilla/5.0 (platform; rv:geckoversion) Gecko/geckotrail Firefox/firefoxversion',
+ },
+ },
+ ],
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ userId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
];
diff --git a/test/integrations/destinations/tiktok_ads_offline_events/processor/data.ts b/test/integrations/destinations/tiktok_ads_offline_events/processor/data.ts
index 2b9341b851..81e125eaca 100644
--- a/test/integrations/destinations/tiktok_ads_offline_events/processor/data.ts
+++ b/test/integrations/destinations/tiktok_ads_offline_events/processor/data.ts
@@ -614,4 +614,248 @@ export const data = [
},
},
},
+ {
+ name: 'tiktok_ads_offline_events',
+ description: 'Test 7 -> `search` standard tiktok Event through event mapping from UI',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: {
+ Config: {
+ accessToken: 'dummyAccessToken',
+ hashUserProperties: true,
+ eventsToStandard: [
+ {
+ from: 'custom event',
+ to: 'Search',
+ },
+ ],
+ },
+ },
+ message: {
+ type: 'track',
+ event: 'custom event',
+ sentAt: '2023-03-22T00:02:33.802Z',
+ traits: {
+ email: [
+ 'efaaf5c8803af4fbf305d7a110c832673d89ed40983770329092fd04b0ba7900',
+ '078d6c8e19f24093368d1712d7801970467f59216f7ccc087bf81b91e0e1f68f',
+ ],
+ phone: [
+ 'c4994d14e724936f1169147dddf1673a09af69b55cc54bc695dbe246bd093b05',
+ '078d6c8e19f24093368d1712d7801970467f59216f7ccc087bf81b91e0e1f68f',
+ ],
+ },
+ userId: '60241286212',
+ channel: 'sources',
+ context: {
+ sources: {
+ job_id: '2N4WuoNQpGYmCPASUvnV86QyhY4/Syncher',
+ version: 'v1.20.0',
+ job_run_id: 'cgd4a063b2fn2e1j0q90',
+ task_run_id: 'cgd4a063b2fn2e1j0qa0',
+ },
+ },
+ recordId: '16322',
+ rudderId: '5b4ed73f-69aa-4198-88d1-3d4d509acbf1',
+ messageId: 'cgd4b663b2fn2e1j8th0',
+ timestamp: '2023-03-22T00:02:33.170Z',
+ properties: {
+ phone: 'c4994d14e724936f1169147dddf1673a09af69b55cc54bc695dbe246bd093b05',
+ value: 32.839999999999996,
+ emails:
+ '["efaaf5c8803af4fbf305d7a110c832673d89ed40983770329092fd04b0ba7900","078d6c8e19f24093368d1712d7801970467f59216f7ccc087bf81b91e0e1f68f","","","","","","","",""]',
+ eventId: '8965fb56-090f-47a5-aa7f-bbab22d9ec90',
+ currency: 'USD',
+ order_id: 60241286212,
+ eventSetId: '7211223771099742210',
+ event_name: 'CompletePayment',
+ },
+ receivedAt: '2023-03-22T00:02:33.171Z',
+ request_ip: '10.7.78.187',
+ anonymousId: '60241286212',
+ originalTimestamp: '2023-03-22T00:02:33.802Z',
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://business-api.tiktok.com/open_api/v1.3/offline/track/',
+ headers: {
+ 'Access-Token': 'dummyAccessToken',
+ 'Content-Type': 'application/json',
+ },
+ params: {},
+ body: {
+ JSON: {
+ event_set_id: '7211223771099742210',
+ event_id: '8965fb56-090f-47a5-aa7f-bbab22d9ec90',
+ timestamp: '2023-03-22T00:02:33.170Z',
+ properties: {
+ order_id: 60241286212,
+ currency: 'USD',
+ value: 32.839999999999996,
+ },
+ event: 'Search',
+ partner_name: 'RudderStack',
+ context: {
+ user: {
+ emails: [
+ '4dc75b075057df6f6b729e74a9feed1244dcf8ceb7903eaba13203f3268ae4b9',
+ '77b639edeb3cd6c801ea05176b8acbfa38d5f38490b764cd0c80756d0cf9ec68',
+ ],
+ phone_numbers: [
+ '28b7b205c2936d2ded022d2587fb2677a76e560e921b3ad615b739b0238baa5d',
+ '77b639edeb3cd6c801ea05176b8acbfa38d5f38490b764cd0c80756d0cf9ec68',
+ ],
+ },
+ },
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ event_set_id: '7211223771099742210',
+ userId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
+ {
+ name: 'tiktok_ads_offline_events',
+ description:
+ 'Test 8 -> `PlaceAnOrder` standard tiktok Event through event `order completed` in payload',
+ feature: 'processor',
+ module: 'destination',
+ version: 'v0',
+ input: {
+ request: {
+ body: [
+ {
+ destination: {
+ Config: {
+ accessToken: 'dummyAccessToken',
+ hashUserProperties: true,
+ eventsToStandard: [],
+ },
+ },
+ message: {
+ type: 'track',
+ event: 'order completed',
+ sentAt: '2023-03-22T00:02:33.802Z',
+ traits: {
+ email: [
+ 'efaaf5c8803af4fbf305d7a110c832673d89ed40983770329092fd04b0ba7900',
+ '078d6c8e19f24093368d1712d7801970467f59216f7ccc087bf81b91e0e1f68f',
+ ],
+ phone: [
+ 'c4994d14e724936f1169147dddf1673a09af69b55cc54bc695dbe246bd093b05',
+ '078d6c8e19f24093368d1712d7801970467f59216f7ccc087bf81b91e0e1f68f',
+ ],
+ },
+ userId: '60241286212',
+ channel: 'sources',
+ context: {
+ sources: {
+ job_id: '2N4WuoNQpGYmCPASUvnV86QyhY4/Syncher',
+ version: 'v1.20.0',
+ job_run_id: 'cgd4a063b2fn2e1j0q90',
+ task_run_id: 'cgd4a063b2fn2e1j0qa0',
+ },
+ },
+ recordId: '16322',
+ rudderId: '5b4ed73f-69aa-4198-88d1-3d4d509acbf1',
+ messageId: 'cgd4b663b2fn2e1j8th0',
+ timestamp: '2023-03-22T00:02:33.170Z',
+ properties: {
+ phone: 'c4994d14e724936f1169147dddf1673a09af69b55cc54bc695dbe246bd093b05',
+ value: 32.839999999999996,
+ emails:
+ '["efaaf5c8803af4fbf305d7a110c832673d89ed40983770329092fd04b0ba7900","078d6c8e19f24093368d1712d7801970467f59216f7ccc087bf81b91e0e1f68f","","","","","","","",""]',
+ eventId: '8965fb56-090f-47a5-aa7f-bbab22d9ec90',
+ currency: 'USD',
+ order_id: 60241286212,
+ eventSetId: '7211223771099742210',
+ event_name: 'CompletePayment',
+ },
+ receivedAt: '2023-03-22T00:02:33.171Z',
+ request_ip: '10.7.78.187',
+ anonymousId: '60241286212',
+ originalTimestamp: '2023-03-22T00:02:33.802Z',
+ },
+ },
+ ],
+ },
+ },
+ output: {
+ response: {
+ status: 200,
+ body: [
+ {
+ output: {
+ version: '1',
+ type: 'REST',
+ method: 'POST',
+ endpoint: 'https://business-api.tiktok.com/open_api/v1.3/offline/track/',
+ headers: {
+ 'Access-Token': 'dummyAccessToken',
+ 'Content-Type': 'application/json',
+ },
+ params: {},
+ body: {
+ JSON: {
+ event_set_id: '7211223771099742210',
+ event_id: '8965fb56-090f-47a5-aa7f-bbab22d9ec90',
+ timestamp: '2023-03-22T00:02:33.170Z',
+ properties: {
+ order_id: 60241286212,
+ currency: 'USD',
+ value: 32.839999999999996,
+ },
+ event: 'PlaceAnOrder',
+ partner_name: 'RudderStack',
+ context: {
+ user: {
+ emails: [
+ '4dc75b075057df6f6b729e74a9feed1244dcf8ceb7903eaba13203f3268ae4b9',
+ '77b639edeb3cd6c801ea05176b8acbfa38d5f38490b764cd0c80756d0cf9ec68',
+ ],
+ phone_numbers: [
+ '28b7b205c2936d2ded022d2587fb2677a76e560e921b3ad615b739b0238baa5d',
+ '77b639edeb3cd6c801ea05176b8acbfa38d5f38490b764cd0c80756d0cf9ec68',
+ ],
+ },
+ },
+ },
+ JSON_ARRAY: {},
+ XML: {},
+ FORM: {},
+ },
+ files: {},
+ event_set_id: '7211223771099742210',
+ userId: '',
+ },
+ statusCode: 200,
+ },
+ ],
+ },
+ },
+ },
];
diff --git a/test/integrations/testUtils.ts b/test/integrations/testUtils.ts
index 683f9dbe3b..2abe4c6d9a 100644
--- a/test/integrations/testUtils.ts
+++ b/test/integrations/testUtils.ts
@@ -149,7 +149,7 @@ export const generateSimplifiedIdentifyPayload: any = (parametersOverride: any)
rudderId: parametersOverride.rudderId || generateAlphanumericId(36),
messageId: parametersOverride.messageId || generateAlphanumericId(36),
context: {
- externalId: parametersOverride.externalId,
+ externalId: parametersOverride.context.externalId,
traits: parametersOverride.context.traits,
},
anonymousId: parametersOverride.anonymousId || 'default-anonymousId',
@@ -204,7 +204,7 @@ export const generateSimplifiedTrackPayload: any = (parametersOverride: any) =>
rudderId: parametersOverride.rudderId || generateAlphanumericId(36),
messageId: parametersOverride.messageId || generateAlphanumericId(36),
context: removeUndefinedAndNullValues({
- externalId: parametersOverride.externalId,
+ externalId: parametersOverride.context.externalId,
traits: parametersOverride.context.traits,
}),
anonymousId: parametersOverride.anonymousId || 'default-anonymousId',
@@ -484,9 +484,9 @@ export const validateTestWithZOD = (testPayload: TestCaseData, response: any) =>
case 'router':
RouterTransformationResponseListSchema.parse(response.body.output);
break;
- // case 'batch':
- // BatchScheam.parse(responseBody);
- // break;
+ case 'batch':
+ RouterTransformationResponseListSchema.parse(response.body);
+ break;
// case 'user_deletion':
// DeletionSchema.parse(responseBody);
// break;
@@ -527,7 +527,9 @@ export const generateMetadata = (jobId: number): any => {
sourceId: 'default-sourceId',
destinationId: 'default-destinationId',
workspaceId: 'default-workspaceId',
- secret: {},
+ secret: {
+ accessToken: 'default-accessToken',
+ },
dontBatch: false,
};
};