diff --git a/src/cdk/v2/destinations/ninetailed/config.js b/src/cdk/v2/destinations/ninetailed/config.js new file mode 100644 index 0000000000..c38496a415 --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/config.js @@ -0,0 +1,31 @@ +const { getMappingConfig } = require('../../../../v0/util'); + +const ConfigCategories = { + GENERAL: { + type: 'general', + name: 'generalPayloadMapping', + }, + CONTEXT: { + type: 'context', + name: 'contextMapping', + }, + TRACK: { + type: 'track', + name: 'trackMapping', + }, + IDENTIFY: { + type: 'identify', + name: 'identifyMapping', + }, + PAGE: { + type: 'page', + name: 'pageMapping', + }, +}; + +// MAX_BATCH_SIZE : // Maximum number of events to send in a single batch +const mappingConfig = getMappingConfig(ConfigCategories, __dirname); +const batchEndpoint = + 'https://experience.ninetailed.co/v2/organizations/{{organisationId}}/environments/{{environment}}/events'; + +module.exports = { ConfigCategories, mappingConfig, batchEndpoint, MAX_BATCH_SIZE: 200 }; diff --git a/src/cdk/v2/destinations/ninetailed/data/contextMapping.json b/src/cdk/v2/destinations/ninetailed/data/contextMapping.json new file mode 100644 index 0000000000..3d6392dd1e --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/data/contextMapping.json @@ -0,0 +1,43 @@ +[ + { + "sourceKeys": "app.name", + "required": true, + "destKey": "app.name" + }, + { + "sourceKeys": "app.version", + "required": true, + "destKey": "app.version" + }, + { + "sourceKeys": "campaign", + "destKey": "campaign" + }, + { + "sourceKeys": "library.name", + "required": true, + "destKey": "library.name" + }, + { + "sourceKeys": "library.version", + "required": true, + "destKey": "library.version" + }, + { + "sourceKeys": "locale", + "destKey": "locale" + }, + { + "sourceKeys": "page", + "destKey": "page" + }, + { + "sourceKeys": "userAgent", + "destKey": "userAgent" + }, + { + "sourceKeys": "location", + "required": true, + "destKey": "location" + } +] diff --git a/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json b/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json new file mode 100644 index 0000000000..3ab72d1b9f --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/data/generalPayloadMapping.json @@ -0,0 +1,25 @@ +[ + { + "sourceKeys": "anonymousId", + "required": true, + "destKey": "anonymousId" + }, + { + "sourceKeys": "messageId", + "required": true, + "destKey": "messageId" + }, + { + "sourceKeys": "channel", + "required": true, + "destKey": "channel" + }, + { + "sourceKeys": "type", + "destKey": "type" + }, + { + "sourceKeys": "originalTimestamp", + "destKey": "originalTimestamp" + } +] diff --git a/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json b/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json new file mode 100644 index 0000000000..e8d3f7797d --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/data/identifyMapping.json @@ -0,0 +1,14 @@ +[ + { + "sourceKeys": "traits", + "sourceFromGenericMap": true, + "required": true, + "destKey": "traits" + }, + { + "sourceKeys": "userIdOnly", + "sourceFromGenericMap": true, + "required": true, + "destKey": "userId" + } +] diff --git a/src/cdk/v2/destinations/ninetailed/data/pageMapping.json b/src/cdk/v2/destinations/ninetailed/data/pageMapping.json new file mode 100644 index 0000000000..80ec2f58f1 --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/data/pageMapping.json @@ -0,0 +1,7 @@ +[ + { + "sourceKeys": "properties", + "required": true, + "destKey": "properties" + } +] diff --git a/src/cdk/v2/destinations/ninetailed/data/trackMapping.json b/src/cdk/v2/destinations/ninetailed/data/trackMapping.json new file mode 100644 index 0000000000..44af6dd1a3 --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/data/trackMapping.json @@ -0,0 +1,12 @@ +[ + { + "sourceKeys": "properties", + "required": true, + "destKey": "properties" + }, + { + "sourceKeys": "event", + "required": true, + "destKey": "event" + } +] diff --git a/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml b/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml new file mode 100644 index 0000000000..6f5056ce10 --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/procWorkflow.yaml @@ -0,0 +1,33 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + - name: defaultRequestConfig + path: ../../../../v0/util + - name: removeUndefinedAndNullValues + path: ../../../../v0/util + - path: ./utils + +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,.PAGE])}}, "message type " + messageType + " is not supported"); + $.assertConfig(.destination.Config.organisationId, "Organisation ID is not present. Aborting"); + $.assertConfig(.destination.Config.environment, "Environment is not present. Aborting"); + - name: preparePayload + template: | + const payload = $.constructFullPayload(.message); + $.context.payload = $.removeUndefinedAndNullValues(payload); + + - name: buildResponse + template: | + const response = $.defaultRequestConfig(); + response.body.JSON.events = [$.context.payload]; + response.endpoint = $.getEndpoint(.destination.Config); + response.method = "POST"; + response diff --git a/src/cdk/v2/destinations/ninetailed/rtWorkflow.yaml b/src/cdk/v2/destinations/ninetailed/rtWorkflow.yaml new file mode 100644 index 0000000000..30dd3fdd95 --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/rtWorkflow.yaml @@ -0,0 +1,35 @@ +bindings: + - path: ./config + - name: handleRtTfSingleEventError + path: ../../../../v0/util/index + - path: ./utils +steps: + - name: validateInput + template: | + $.assert(Array.isArray(^) && ^.length > 0, "Invalid event array") + + - name: transform + externalWorkflow: + path: ./procWorkflow.yaml + loopOverInput: true + + - name: successfulEvents + template: | + $.outputs.transform#idx.output.({ + "output": .body.JSON.events[0], + "destination": ^[idx].destination, + "metadata": ^[idx].metadata + })[] + - name: failedEvents + template: | + $.outputs.transform#idx.error.( + $.handleRtTfSingleEventError(^[idx], .originalError ?? ., {}) + )[] + - name: batchSuccessfulEvents + description: Batches the successfulEvents + template: | + $.batchResponseBuilder($.outputs.successfulEvents); + + - name: finalPayload + template: | + [...$.outputs.failedEvents, ...$.outputs.batchSuccessfulEvents] diff --git a/src/cdk/v2/destinations/ninetailed/utils.js b/src/cdk/v2/destinations/ninetailed/utils.js new file mode 100644 index 0000000000..b716422a0e --- /dev/null +++ b/src/cdk/v2/destinations/ninetailed/utils.js @@ -0,0 +1,109 @@ +const { BatchUtils } = require('@rudderstack/workflow-engine'); +const config = require('./config'); +const { constructPayload } = require('../../../../v0/util'); + +/** + * This fucntion constructs payloads based upon mappingConfig for all calls + * We build context as it has some specific payloads with default values so just breaking them down + * @param {*} message + * @returns + */ +const constructFullPayload = (message) => { + const context = constructPayload( + message?.context || {}, + config.mappingConfig[config.ConfigCategories.CONTEXT.name], + ); + const payload = constructPayload( + message, + config.mappingConfig[config.ConfigCategories.GENERAL.name], + ); + let typeSpecifcPayload; + switch (message.type) { + case 'track': + typeSpecifcPayload = constructPayload( + message, + config.mappingConfig[config.ConfigCategories.TRACK.name], + ); + break; + case 'identify': + typeSpecifcPayload = constructPayload( + message, + config.mappingConfig[config.ConfigCategories.IDENTIFY.name], + ); + break; + case 'page': + typeSpecifcPayload = constructPayload( + message, + config.mappingConfig[config.ConfigCategories.PAGE.name], + ); + break; + default: + break; + } + payload.context = context; + return { ...payload, ...typeSpecifcPayload }; // merge base and type-specific payloads; +}; + +const getEndpoint = (Config) => { + const { organisationId, environment } = Config; + return config.batchEndpoint + .replace('{{organisationId}}', organisationId) + .replace('{{environment}}', environment); +}; + +const mergeMetadata = (batch) => { + const metadata = []; + batch.forEach((event) => { + metadata.push(event.metadata); + }); + return metadata; +}; + +const getMergedEvents = (batch) => { + const events = []; + batch.forEach((event) => { + events.push(event.output); + }); + return events; +}; + +const batchBuilder = (batch) => ({ + batchedRequest: { + body: { + JSON: { events: getMergedEvents(batch) }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: getEndpoint(batch[0].destination.Config), + headers: { + 'Content-Type': 'application/json', + }, + params: {}, + files: {}, + }, + metadata: mergeMetadata(batch), + batched: true, + statusCode: 200, + destination: batch[0].destination, +}); + +/** + * This fucntions make chunk of successful events based on MAX_BATCH_SIZE + * and then build the response for each chunk to be returned as object of an array + * @param {*} events + * @returns + */ +const batchResponseBuilder = (events) => { + const batches = BatchUtils.chunkArrayBySizeAndLength(events, { maxItems: config.MAX_BATCH_SIZE }); + const response = []; + batches.items.forEach((batch) => { + response.push(batchBuilder(batch)); + }); + return response; +}; + +module.exports = { constructFullPayload, getEndpoint, batchResponseBuilder }; diff --git a/src/features.json b/src/features.json index 8709dce432..5460111a22 100644 --- a/src/features.json +++ b/src/features.json @@ -65,7 +65,8 @@ "TIKTOK_AUDIENCE": true, "REDDIT": true, "THE_TRADE_DESK": true, - "INTERCOM": true + "INTERCOM": true, + "NINETAILED": true }, "regulations": [ "BRAZE", diff --git a/src/v0/destinations/adobe_analytics/transform.js b/src/v0/destinations/adobe_analytics/transform.js index b428138724..5d3d6e7d00 100644 --- a/src/v0/destinations/adobe_analytics/transform.js +++ b/src/v0/destinations/adobe_analytics/transform.js @@ -18,6 +18,7 @@ const { getIntegrationsObj, removeUndefinedAndNullValues, simpleProcessRouterDest, + validateEventAndLowerCaseConversion, } = require('../../util'); const { @@ -307,7 +308,7 @@ const processTrackEvent = (message, adobeEventName, destinationConfig, extras = destinationConfig; const { event: rawMessageEvent, properties } = message; const { overrideEventString, overrideProductString } = properties; - const event = rawMessageEvent.toLowerCase(); + const event = validateEventAndLowerCaseConversion(rawMessageEvent, true, true); const adobeEventArr = adobeEventName ? adobeEventName.split(',') : []; // adobeEventArr is an array of events which is defined as // ["eventName", "mapped Adobe Event=mapped merchproperty's value", "mapped Adobe Event=mapped merchproperty's value", . . .] diff --git a/src/v0/destinations/facebook_conversions/utils.js b/src/v0/destinations/facebook_conversions/utils.js index 26204ec61a..c6e3993e33 100644 --- a/src/v0/destinations/facebook_conversions/utils.js +++ b/src/v0/destinations/facebook_conversions/utils.js @@ -93,28 +93,26 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego ); const contentCategory = eventTypeCustomData.content_category; - let contentType; + let defaultContentType; if (contentIds.length > 0) { - contentType = 'product'; + defaultContentType = 'product'; } else if (contentCategory) { contentIds.push(contentCategory); contents.push({ id: contentCategory, quantity: 1, }); - contentType = 'product_group'; + defaultContentType = 'product_group'; } + const contentType = + message.properties?.content_type || + getContentType(message, defaultContentType, categoryToContent, DESTINATION.toLowerCase()); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds, contents, - content_type: getContentType( - message, - contentType, - categoryToContent, - DESTINATION.toLowerCase(), - ), + content_type: contentType, content_category: getContentCategory(contentCategory), }; break; @@ -125,18 +123,20 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego case 'payment info entered': case 'product added to wishlist': { const contentCategory = eventTypeCustomData.content_category; - const contentType = eventTypeCustomData.content_type; + const contentType = + message.properties?.content_type || + getContentType( + message, + eventTypeCustomData.content_type, + categoryToContent, + DESTINATION.toLowerCase(), + ); const { contentIds, contents } = populateContentsAndContentIDs([message.properties]); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds, contents, - content_type: getContentType( - message, - contentType, - categoryToContent, - DESTINATION.toLowerCase(), - ), + content_type: contentType, content_category: getContentCategory(contentCategory), }; validateProductSearchedData(eventTypeCustomData); @@ -151,18 +151,20 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego ); const contentCategory = eventTypeCustomData.content_category; - const contentType = eventTypeCustomData.content_type; + const contentType = + message.properties?.content_type || + getContentType( + message, + eventTypeCustomData.content_type, + categoryToContent, + DESTINATION.toLowerCase(), + ); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds, contents, - content_type: getContentType( - message, - contentType, - categoryToContent, - DESTINATION.toLowerCase(), - ), + content_type: contentType, content_category: getContentCategory(contentCategory), num_items: contentIds.length, }; diff --git a/src/v0/destinations/facebook_pixel/utils.js b/src/v0/destinations/facebook_pixel/utils.js index 8a63a0b0fe..cfa625ee3d 100644 --- a/src/v0/destinations/facebook_pixel/utils.js +++ b/src/v0/destinations/facebook_pixel/utils.js @@ -53,13 +53,9 @@ const getActionSource = (payload, channel) => { * Handles order completed and checkout started types of specific events */ const handleOrder = (message, categoryToContent) => { - const { products, revenue } = message.properties; - const value = formatRevenue(revenue); - - const contentType = getContentType(message, 'product', categoryToContent); - const contentIds = []; - const contents = []; const { + products, + revenue, category, quantity, price, @@ -67,6 +63,12 @@ const handleOrder = (message, categoryToContent) => { contentName, delivery_category: deliveryCategory, } = message.properties; + const value = formatRevenue(revenue); + let { content_type: contentType } = message.properties; + contentType = contentType || getContentType(message, 'product', categoryToContent); + const contentIds = []; + const contents = []; + if (products) { if (products.length > 0 && Array.isArray(products)) { products.forEach((singleProduct) => { @@ -109,10 +111,17 @@ const handleOrder = (message, categoryToContent) => { * Handles product list viewed */ const handleProductListViewed = (message, categoryToContent) => { - let contentType; + let defaultContentType; const contentIds = []; const contents = []; - const { products, category, quantity, value, contentName } = message.properties; + const { + products, + category, + quantity, + value, + contentName, + content_type: contentType, + } = message.properties; if (products && products.length > 0 && Array.isArray(products)) { products.forEach((product, index) => { if (isObject(product)) { @@ -132,7 +141,7 @@ const handleProductListViewed = (message, categoryToContent) => { } if (contentIds.length > 0) { - contentType = 'product'; + defaultContentType = 'product'; // for viewContent event content_ids and content arrays are not mandatory } else if (category) { contentIds.push(category); @@ -140,12 +149,12 @@ const handleProductListViewed = (message, categoryToContent) => { id: category, quantity: 1, }); - contentType = 'product_group'; + defaultContentType = 'product_group'; } return { content_ids: contentIds, - content_type: getContentType(message, contentType, categoryToContent), + content_type: contentType || getContentType(message, defaultContentType, categoryToContent), contents, content_category: getContentCategory(category), content_name: contentName, @@ -165,7 +174,8 @@ const handleProduct = (message, categoryToContent, valueFieldIdentifier) => { const useValue = valueFieldIdentifier === 'properties.value'; const contentId = message.properties?.product_id || message.properties?.sku || message.properties?.id; - const contentType = getContentType(message, 'product', categoryToContent); + const contentType = + message.properties?.content_type || getContentType(message, 'product', categoryToContent); const contentName = message.properties.product_name || message.properties.name || ''; const contentCategory = message.properties.category || ''; const currency = message.properties.currency || 'USD'; diff --git a/src/v0/destinations/google_adwords_remarketing_lists/transform.js b/src/v0/destinations/google_adwords_remarketing_lists/transform.js index 9526973fb8..9ab415346a 100644 --- a/src/v0/destinations/google_adwords_remarketing_lists/transform.js +++ b/src/v0/destinations/google_adwords_remarketing_lists/transform.js @@ -218,7 +218,7 @@ const processEvent = async (metadata, message, destination) => { } Object.values(createdPayload).forEach((data) => { - const consentObj = populateConsentForGoogleDestinations(message.properties); + const consentObj = populateConsentForGoogleDestinations(destination.Config); response.push(responseBuilder(metadata, data, destination, message, consentObj)); }); return response; diff --git a/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json b/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json index e128f2666c..b358919bc1 100644 --- a/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json +++ b/src/v0/destinations/klaviyo/data/KlaviyoIdentify.json @@ -57,7 +57,12 @@ "traits.address.region", "context.traits.region", "context.traits.address.region", - "properties.region" + "properties.region", + "traits.state", + "traits.address.state", + "context.traits.address.state", + "context.traits.state", + "properties.state" ], "required": false }, @@ -77,14 +82,19 @@ "sourceKeys": [ "traits.zip", "traits.postalcode", + "traits.postalCode", "traits.address.zip", "traits.address.postalcode", + "traits.address.postalCode", "context.traits.zip", "context.traits.postalcode", + "context.traits.postalCode", "context.traits.address.zip", "context.traits.address.postalcode", + "context.traits.address.postalCode", "properties.zip", - "properties.postalcode" + "properties.postalcode", + "properties.postalCode" ], "required": false }, @@ -97,5 +107,16 @@ "destKey": "location.timezone", "sourceKeys": ["traits.timezone", "context.traits.timezone", "properties.timezone"], "required": false + }, + { + "destKey": "location.address1", + "sourceKeys": [ + "traits.street", + "traits.address.street", + "context.traits.street", + "context.traits.address.street", + "properties.street" + ], + "required": false } ] diff --git a/src/v0/destinations/klaviyo/data/KlaviyoProfile.json b/src/v0/destinations/klaviyo/data/KlaviyoProfile.json index e2a8d86085..329ecd978f 100644 --- a/src/v0/destinations/klaviyo/data/KlaviyoProfile.json +++ b/src/v0/destinations/klaviyo/data/KlaviyoProfile.json @@ -41,7 +41,12 @@ "traits.address.region", "context.traits.region", "context.traits.address.region", - "properties.region" + "properties.region", + "traits.state", + "traits.address.state", + "context.traits.address.state", + "context.traits.state", + "properties.state" ], "required": false }, @@ -61,14 +66,19 @@ "sourceKeys": [ "traits.zip", "traits.postalcode", + "traits.postalCode", "traits.address.zip", "traits.address.postalcode", + "traits.address.postalCode", "context.traits.zip", "context.traits.postalcode", + "context.traits.postalCode", "context.traits.address.zip", "context.traits.address.postalcode", + "context.traits.address.postalCode", "properties.zip", - "properties.postalcode" + "properties.postalcode", + "properties.postalCode" ], "required": false }, @@ -81,5 +91,16 @@ "destKey": "$image", "sourceKeys": ["traits.image", "context.traits.image", "properties.image"], "required": false + }, + { + "destKey": "$address1", + "sourceKeys": [ + "traits.street", + "traits.address.street", + "context.traits.street", + "context.traits.address.street", + "properties.street" + ], + "required": false } ] diff --git a/src/v0/destinations/mp/transform.js b/src/v0/destinations/mp/transform.js index 493169cd4e..10271bebef 100644 --- a/src/v0/destinations/mp/transform.js +++ b/src/v0/destinations/mp/transform.js @@ -36,6 +36,7 @@ const { groupEventsByEndpoint, batchEvents, trimTraits, + generatePageOrScreenCustomEventName, } = require('./util'); const { CommonUtils } = require('../../../util/common'); @@ -297,17 +298,25 @@ const processIdentifyEvents = async (message, type, destination) => { }; const processPageOrScreenEvents = (message, type, destination) => { + const { + token, + identityMergeApi, + useUserDefinedPageEventName, + userDefinedPageEventTemplate, + useUserDefinedScreenEventName, + userDefinedScreenEventTemplate, + } = destination.Config; const mappedProperties = constructPayload(message, mPEventPropertiesConfigJson); let properties = { ...get(message, 'context.traits'), ...message.properties, ...mappedProperties, - token: destination.Config.token, + token, distinct_id: message.userId || message.anonymousId, time: toUnixTimestampInMS(message.timestamp || message.originalTimestamp), ...buildUtmParams(message.context?.campaign), }; - if (destination.Config?.identityMergeApi === 'simplified') { + if (identityMergeApi === 'simplified') { properties = { ...properties, distinct_id: message.userId || `$device:${message.anonymousId}`, @@ -326,7 +335,18 @@ const processPageOrScreenEvents = (message, type, destination) => { properties.$browser = browser.name; properties.$browser_version = browser.version; } - const eventName = type === 'page' ? 'Loaded a Page' : 'Loaded a Screen'; + + let eventName; + if (type === 'page') { + eventName = useUserDefinedPageEventName + ? generatePageOrScreenCustomEventName(message, userDefinedPageEventTemplate) + : 'Loaded a Page'; + } else { + eventName = useUserDefinedScreenEventName + ? generatePageOrScreenCustomEventName(message, userDefinedScreenEventTemplate) + : 'Loaded a Screen'; + } + const payload = { event: eventName, properties, diff --git a/src/v0/destinations/mp/util.js b/src/v0/destinations/mp/util.js index 8e943f41dd..f56242d88b 100644 --- a/src/v0/destinations/mp/util.js +++ b/src/v0/destinations/mp/util.js @@ -1,7 +1,7 @@ const lodash = require('lodash'); const set = require('set-value'); const get = require('get-value'); -const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); const { isDefined, constructPayload, @@ -16,6 +16,7 @@ const { IsGzipSupported, isObject, isDefinedAndNotNullAndNotEmpty, + isDefinedAndNotNull, } = require('../../util'); const { ConfigCategory, @@ -301,6 +302,46 @@ function trimTraits(traits, contextTraits, setOnceProperties) { }; } +/** + * Generates a custom event name for a page or screen. + * + * @param {Object} message - The message object + * @param {string} userDefinedEventTemplate - The user-defined event template to be used for generating the event name. + * @throws {ConfigurationError} If the event template is missing. + * @returns {string} The generated custom event name. + * @example + * const userDefinedEventTemplate = "Viewed {{ category }} {{ name }} Page"; + * const message = {name: 'Home', properties: {category: 'Index'}}; + * output: "Viewed Index Home Page" + */ +const generatePageOrScreenCustomEventName = (message, userDefinedEventTemplate) => { + if (!userDefinedEventTemplate) { + throw new ConfigurationError( + 'Event name template is not configured. Please provide a valid value for the `Page/Screen Event Name Template` in the destination dashboard.', + ); + } + + let eventName = userDefinedEventTemplate; + + if (isDefinedAndNotNull(message.properties?.category)) { + // Replace {{ category }} with actual values + eventName = eventName.replace(/{{\s*category\s*}}/g, message.properties.category); + } else { + // find {{ category }} surrounded by whitespace characters and replace it with a single whitespace character + eventName = eventName.replace(/\s{{\s*category\s*}}\s/g, ' '); + } + + if (isDefinedAndNotNull(message.name)) { + // Replace {{ name }} with actual values + eventName = eventName.replace(/{{\s*name\s*}}/g, message.name); + } else { + // find {{ name }} surrounded by whitespace characters and replace it with a single whitespace character + eventName = eventName.replace(/\s{{\s*name\s*}}\s/g, ' '); + } + + return eventName; +}; + module.exports = { createIdentifyResponse, isImportAuthCredentialsAvailable, @@ -309,4 +350,5 @@ module.exports = { generateBatchedPayloadForArray, batchEvents, trimTraits, + generatePageOrScreenCustomEventName, }; diff --git a/src/v0/destinations/mp/util.test.js b/src/v0/destinations/mp/util.test.js index 866119a336..40cdb34649 100644 --- a/src/v0/destinations/mp/util.test.js +++ b/src/v0/destinations/mp/util.test.js @@ -4,45 +4,88 @@ const { generateBatchedPayloadForArray, buildUtmParams, trimTraits, + generatePageOrScreenCustomEventName, } = require('./util'); const { FEATURE_GZIP_SUPPORT } = require('../../util/constant'); - -const destinationMock = { - Config: { - token: 'test_api_token', - prefixProperties: true, - useNativeSDK: false, - useOldMapping: true, - }, - DestinationDefinition: { - DisplayName: 'Mixpanel', - ID: 'test_destination_definition_id', - Name: 'MP', - }, - Enabled: true, - ID: 'test_id', - Name: 'Mixpanel', - Transformations: [], -}; +const { ConfigurationError } = require('@rudderstack/integrations-lib'); const maxBatchSizeMock = 2; -describe('Mixpanel utils test', () => { - describe('Unit test cases for groupEventsByEndpoint', () => { - it('should return an object with empty arrays for all properties when the events array is empty', () => { - const events = []; - const result = groupEventsByEndpoint(events); - expect(result).toEqual({ - engageEvents: [], - groupsEvents: [], - trackEvents: [], - importEvents: [], - batchErrorRespList: [], - }); +describe('Unit test cases for groupEventsByEndpoint', () => { + it('should return an object with empty arrays for all properties when the events array is empty', () => { + const events = []; + const result = groupEventsByEndpoint(events); + expect(result).toEqual({ + engageEvents: [], + groupsEvents: [], + trackEvents: [], + importEvents: [], + batchErrorRespList: [], }); + }); - it('should return an object with all properties containing their respective events when the events array contains events of all types', () => { - const events = [ + it('should return an object with all properties containing their respective events when the events array contains events of all types', () => { + const events = [ + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{prop:1}]', + }, + }, + userId: 'user1', + }, + }, + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{prop:2}]', + }, + }, + userId: 'user2', + }, + }, + { + message: { + endpoint: '/groups', + body: { + JSON_ARRAY: { + batch: '[{prop:3}]', + }, + }, + userId: 'user1', + }, + }, + { + message: { + endpoint: '/track', + body: { + JSON_ARRAY: { + batch: '[{prop:4}]', + }, + }, + userId: 'user1', + }, + }, + { + message: { + endpoint: '/import', + body: { + JSON_ARRAY: { + batch: '[{prop:5}]', + }, + }, + userId: 'user2', + }, + }, + { error: 'Message type abc not supported' }, + ]; + const result = groupEventsByEndpoint(events); + expect(result).toEqual({ + engageEvents: [ { message: { endpoint: '/engage', @@ -65,6 +108,8 @@ describe('Mixpanel utils test', () => { userId: 'user2', }, }, + ], + groupsEvents: [ { message: { endpoint: '/groups', @@ -76,6 +121,8 @@ describe('Mixpanel utils test', () => { userId: 'user1', }, }, + ], + trackEvents: [ { message: { endpoint: '/track', @@ -87,6 +134,8 @@ describe('Mixpanel utils test', () => { userId: 'user1', }, }, + ], + importEvents: [ { message: { endpoint: '/import', @@ -98,371 +147,359 @@ describe('Mixpanel utils test', () => { userId: 'user2', }, }, - { error: 'Message type abc not supported' }, - ]; - const result = groupEventsByEndpoint(events); - expect(result).toEqual({ - engageEvents: [ - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{prop:1}]', - }, - }, - userId: 'user1', - }, - }, - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{prop:2}]', - }, - }, - userId: 'user2', - }, - }, - ], - groupsEvents: [ - { - message: { - endpoint: '/groups', - body: { - JSON_ARRAY: { - batch: '[{prop:3}]', - }, - }, - userId: 'user1', - }, - }, - ], - trackEvents: [ - { - message: { - endpoint: '/track', - body: { - JSON_ARRAY: { - batch: '[{prop:4}]', - }, - }, - userId: 'user1', - }, - }, - ], - importEvents: [ - { - message: { - endpoint: '/import', - body: { - JSON_ARRAY: { - batch: '[{prop:5}]', - }, - }, - userId: 'user2', - }, - }, - ], - batchErrorRespList: [{ error: 'Message type abc not supported' }], - }); + ], + batchErrorRespList: [{ error: 'Message type abc not supported' }], }); }); +}); - describe('Unit test cases for batchEvents', () => { - it('should return an array of batched events with correct payload and metadata', () => { - const successRespList = [ - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{"prop":1}]', - }, +describe('Unit test cases for batchEvents', () => { + it('should return an array of batched events with correct payload and metadata', () => { + const successRespList = [ + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{"prop":1}]', }, - headers: {}, - params: {}, - userId: 'user1', }, - metadata: { jobId: 3 }, + headers: {}, + params: {}, + userId: 'user1', }, - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{"prop":2}]', - }, + metadata: { jobId: 3 }, + }, + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{"prop":2}]', }, - headers: {}, - params: {}, - userId: 'user2', }, - metadata: { jobId: 4 }, + headers: {}, + params: {}, + userId: 'user2', }, - { - message: { - endpoint: '/engage', - body: { - JSON_ARRAY: { - batch: '[{"prop":3}]', - }, + metadata: { jobId: 4 }, + }, + { + message: { + endpoint: '/engage', + body: { + JSON_ARRAY: { + batch: '[{"prop":3}]', }, - headers: {}, - params: {}, - userId: 'user2', }, - metadata: { jobId: 6 }, + headers: {}, + params: {}, + userId: 'user2', }, - ]; - - const result = batchEvents(successRespList, maxBatchSizeMock); - - expect(result).toEqual([ - { - batched: true, - batchedRequest: { - body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":1},{"prop":2}]' }, XML: {} }, - endpoint: '/engage', - files: {}, - headers: {}, - method: 'POST', - params: {}, - type: 'REST', - version: '1', - }, - destination: undefined, - metadata: [{ jobId: 3 }, { jobId: 4 }], - statusCode: 200, + metadata: { jobId: 6 }, + }, + ]; + + const result = batchEvents(successRespList, maxBatchSizeMock); + + expect(result).toEqual([ + { + batched: true, + batchedRequest: { + body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":1},{"prop":2}]' }, XML: {} }, + endpoint: '/engage', + files: {}, + headers: {}, + method: 'POST', + params: {}, + type: 'REST', + version: '1', }, - { - batched: true, - batchedRequest: { - body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":3}]' }, XML: {} }, - endpoint: '/engage', - files: {}, - headers: {}, - method: 'POST', - params: {}, - type: 'REST', - version: '1', - }, - destination: undefined, - metadata: [{ jobId: 6 }], - statusCode: 200, + destination: undefined, + metadata: [{ jobId: 3 }, { jobId: 4 }], + statusCode: 200, + }, + { + batched: true, + batchedRequest: { + body: { FORM: {}, JSON: {}, JSON_ARRAY: { batch: '[{"prop":3}]' }, XML: {} }, + endpoint: '/engage', + files: {}, + headers: {}, + method: 'POST', + params: {}, + type: 'REST', + version: '1', }, - ]); - }); + destination: undefined, + metadata: [{ jobId: 6 }], + statusCode: 200, + }, + ]); + }); - it('should return an empty array when successRespList is empty', () => { - const successRespList = []; - const result = batchEvents(successRespList, maxBatchSizeMock); - expect(result).toEqual([]); - }); + it('should return an empty array when successRespList is empty', () => { + const successRespList = []; + const result = batchEvents(successRespList, maxBatchSizeMock); + expect(result).toEqual([]); }); +}); - 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 = [ - { - body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, - endpoint: '/import', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - { - body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, - endpoint: '/import', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - ]; - const expectedBatchedRequest = { - body: { - FORM: {}, - JSON: {}, - JSON_ARRAY: {}, - XML: {}, - GZIP: { - payload: '[{"event":"event1"},{"event":"event2"}]', - }, - }, +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 = [ + { + body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, endpoint: '/import', - files: {}, headers: { 'Content-Type': 'application/json' }, - method: 'POST', params: {}, - type: 'REST', - version: '1', - }; - - const result = generateBatchedPayloadForArray(events, { - features: { [FEATURE_GZIP_SUPPORT]: true }, - }); - - expect(result).toEqual(expectedBatchedRequest); + }, + { + body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, + endpoint: '/import', + headers: { 'Content-Type': 'application/json' }, + params: {}, + }, + ]; + const expectedBatchedRequest = { + body: { + FORM: {}, + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + GZIP: { + payload: '[{"event":"event1"},{"event":"event2"}]', + }, + }, + endpoint: '/import', + files: {}, + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }; + + const result = generateBatchedPayloadForArray(events, { + features: { [FEATURE_GZIP_SUPPORT]: true }, }); - it('should generate a batched payload with JSON_ARRAY body when given an array of events', () => { - const events = [ - { - body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, - endpoint: '/endpoint', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - { - body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, - endpoint: '/endpoint', - headers: { 'Content-Type': 'application/json' }, - params: {}, - }, - ]; - const expectedBatchedRequest = { - body: { - FORM: {}, - JSON: {}, - JSON_ARRAY: { batch: '[{"event":"event1"},{"event":"event2"}]' }, - XML: {}, - }, + expect(result).toEqual(expectedBatchedRequest); + }); + + it('should generate a batched payload with JSON_ARRAY body when given an array of events', () => { + const events = [ + { + body: { JSON_ARRAY: { batch: '[{"event": "event1"}]' } }, endpoint: '/endpoint', - files: {}, headers: { 'Content-Type': 'application/json' }, - method: 'POST', params: {}, - type: 'REST', - version: '1', - }; - - const result = generateBatchedPayloadForArray(events, { - features: { [FEATURE_GZIP_SUPPORT]: true }, - }); - - expect(result).toEqual(expectedBatchedRequest); + }, + { + body: { JSON_ARRAY: { batch: '[{"event": "event2"}]' } }, + endpoint: '/endpoint', + headers: { 'Content-Type': 'application/json' }, + params: {}, + }, + ]; + const expectedBatchedRequest = { + body: { + FORM: {}, + JSON: {}, + JSON_ARRAY: { batch: '[{"event":"event1"},{"event":"event2"}]' }, + XML: {}, + }, + endpoint: '/endpoint', + files: {}, + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }; + + const result = generateBatchedPayloadForArray(events, { + features: { [FEATURE_GZIP_SUPPORT]: true }, }); + + expect(result).toEqual(expectedBatchedRequest); }); +}); - describe('Unit test cases for buildUtmParams', () => { - it('should return an empty object when campaign is undefined', () => { - const campaign = undefined; - const result = buildUtmParams(campaign); - expect(result).toEqual({}); - }); +describe('Unit test cases for buildUtmParams', () => { + it('should return an empty object when campaign is undefined', () => { + const campaign = undefined; + const result = buildUtmParams(campaign); + expect(result).toEqual({}); + }); - it('should return an empty object when campaign is an empty object', () => { - const campaign = {}; - const result = buildUtmParams(campaign); - expect(result).toEqual({}); - }); + it('should return an empty object when campaign is an empty object', () => { + const campaign = {}; + const result = buildUtmParams(campaign); + expect(result).toEqual({}); + }); - it('should return an empty object when campaign is not an object', () => { - const campaign = [{ name: 'test' }]; - const result = buildUtmParams(campaign); - expect(result).toEqual({}); - }); + it('should return an empty object when campaign is not an object', () => { + const campaign = [{ name: 'test' }]; + const result = buildUtmParams(campaign); + expect(result).toEqual({}); + }); - it('should handle campaign object with null/undefined values', () => { - const campaign = { name: null, source: 'rudder', medium: 'rudder', test: undefined }; - const result = buildUtmParams(campaign); - expect(result).toEqual({ - utm_campaign: null, - utm_source: 'rudder', - utm_medium: 'rudder', - test: undefined, - }); + it('should handle campaign object with null/undefined values', () => { + const campaign = { name: null, source: 'rudder', medium: 'rudder', test: undefined }; + const result = buildUtmParams(campaign); + expect(result).toEqual({ + utm_campaign: null, + utm_source: 'rudder', + utm_medium: 'rudder', + test: undefined, }); }); - describe('Unit test cases for trimTraits', () => { - // Given a valid traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing traits, contextTraits, and setOnce properties. - it('should return an object containing traits, contextTraits, and setOnce properties when given valid inputs', () => { - const traits = { name: 'John', age: 30 }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email']; - - const result = trimTraits(traits, contextTraits, setOnceProperties); - - expect(result).toEqual({ - traits: { - age: 30, - }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com' }, - }); +}); +describe('Unit test cases for trimTraits', () => { + // Given a valid traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing traits, contextTraits, and setOnce properties. + it('should return an object containing traits, contextTraits, and setOnce properties when given valid inputs', () => { + const traits = { name: 'John', age: 30 }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email']; + + const result = trimTraits(traits, contextTraits, setOnceProperties); + + expect(result).toEqual({ + traits: { + age: 30, + }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com' }, }); + }); - // Given an empty traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing empty traits and contextTraits, and an empty setOnce property. - it('should return an object containing empty traits and contextTraits, and an empty setOnce property when given empty traits and contextTraits objects', () => { - const traits = {}; - const contextTraits = {}; - const setOnceProperties = ['name', 'email']; + // Given an empty traits object and contextTraits object, and a valid setOnceProperties array, the function should return an object containing empty traits and contextTraits, and an empty setOnce property. + it('should return an object containing empty traits and contextTraits, and an empty setOnce property when given empty traits and contextTraits objects', () => { + const traits = {}; + const contextTraits = {}; + const setOnceProperties = ['name', 'email']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: {}, - contextTraits: {}, - setOnce: {}, - }); + expect(result).toEqual({ + traits: {}, + contextTraits: {}, + setOnce: {}, }); + }); - // Given an empty setOnceProperties array, the function should return an object containing the original traits and contextTraits objects, and an empty setOnce property. - it('should return an object containing the original traits and contextTraits objects, and an empty setOnce property when given an empty setOnceProperties array', () => { - const traits = { name: 'John', age: 30 }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = []; + // Given an empty setOnceProperties array, the function should return an object containing the original traits and contextTraits objects, and an empty setOnce property. + it('should return an object containing the original traits and contextTraits objects, and an empty setOnce property when given an empty setOnceProperties array', () => { + const traits = { name: 'John', age: 30 }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = []; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { name: 'John', age: 30 }, - contextTraits: { email: 'john@example.com' }, - setOnce: {}, - }); + expect(result).toEqual({ + traits: { name: 'John', age: 30 }, + contextTraits: { email: 'john@example.com' }, + setOnce: {}, }); + }); - // Given a setOnceProperties array containing properties that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. - it('should not add properties to the setOnce property when given setOnceProperties array with non-existent properties', () => { - const traits = { name: 'John', age: 30 }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email', 'address']; + // Given a setOnceProperties array containing properties that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. + it('should not add properties to the setOnce property when given setOnceProperties array with non-existent properties', () => { + const traits = { name: 'John', age: 30 }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email', 'address']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { age: 30 }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com' }, - }); + expect(result).toEqual({ + traits: { age: 30 }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com' }, }); + }); - // Given a setOnceProperties array containing properties with nested paths that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. - it('should not add properties to the setOnce property when given setOnceProperties array with non-existent nested properties', () => { - const traits = { name: 'John', age: 30, address: 'kolkata' }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email', 'address.city']; + // Given a setOnceProperties array containing properties with nested paths that do not exist in either traits or contextTraits objects, the function should not add the property to the setOnce property. + it('should not add properties to the setOnce property when given setOnceProperties array with non-existent nested properties', () => { + const traits = { name: 'John', age: 30, address: 'kolkata' }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email', 'address.city']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { age: 30, address: 'kolkata' }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com' }, - }); + expect(result).toEqual({ + traits: { age: 30, address: 'kolkata' }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com' }, }); + }); - it('should add properties to the setOnce property when given setOnceProperties array with existent nested properties', () => { - const traits = { name: 'John', age: 30, address: { city: 'kolkata' }, isAdult: false }; - const contextTraits = { email: 'john@example.com' }; - const setOnceProperties = ['name', 'email', 'address.city']; + it('should add properties to the setOnce property when given setOnceProperties array with existent nested properties', () => { + const traits = { name: 'John', age: 30, address: { city: 'kolkata' }, isAdult: false }; + const contextTraits = { email: 'john@example.com' }; + const setOnceProperties = ['name', 'email', 'address.city']; - const result = trimTraits(traits, contextTraits, setOnceProperties); + const result = trimTraits(traits, contextTraits, setOnceProperties); - expect(result).toEqual({ - traits: { age: 30, address: {}, isAdult: false }, - contextTraits: {}, - setOnce: { $name: 'John', $email: 'john@example.com', $city: 'kolkata' }, - }); + expect(result).toEqual({ + traits: { age: 30, address: {}, isAdult: false }, + contextTraits: {}, + setOnce: { $name: 'John', $email: 'john@example.com', $city: 'kolkata' }, }); }); }); + +describe('generatePageOrScreenCustomEventName', () => { + it('should throw a ConfigurationError when userDefinedEventTemplate is not provided', () => { + const message = { name: 'Home' }; + const userDefinedEventTemplate = undefined; + expect(() => { + generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + }).toThrow(ConfigurationError); + }); + + it('should generate a custom event name when userDefinedEventTemplate contains event template and message object is provided', () => { + let message = { name: 'Doc', properties: { category: 'Integration' } }; + const userDefinedEventTemplate = 'Viewed {{ category }} {{ name }} page'; + let expected = 'Viewed Integration Doc page'; + let result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + + message = { name: true, properties: { category: 0 } }; + expected = 'Viewed 0 true page'; + result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should generate a custom event name when userDefinedEventTemplate contains event template and category or name is missing in message object', () => { + const message = { name: 'Doc', properties: { category: undefined } }; + const userDefinedEventTemplate = 'Viewed {{ category }} {{ name }} page someKeyword'; + const expected = 'Viewed Doc page someKeyword'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should generate a custom event name when userDefinedEventTemplate contains only category or name placeholder and message object is provided', () => { + const message = { name: 'Doc', properties: { category: 'Integration' } }; + const userDefinedEventTemplate = 'Viewed {{ name }} page'; + const expected = 'Viewed Doc page'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should return the userDefinedEventTemplate when it does not contain placeholder {{}}', () => { + const message = { name: 'Index' }; + const userDefinedEventTemplate = 'Viewed a Home page'; + const expected = 'Viewed a Home page'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); + + it('should return a event name when message object is not provided/empty', () => { + const message = {}; + const userDefinedEventTemplate = 'Viewed {{ category }} {{ name }} page someKeyword'; + const expected = 'Viewed page someKeyword'; + const result = generatePageOrScreenCustomEventName(message, userDefinedEventTemplate); + expect(result).toBe(expected); + }); +}); diff --git a/src/v0/destinations/one_signal/transform.js b/src/v0/destinations/one_signal/transform.js index a072aef0e4..b025660fa4 100644 --- a/src/v0/destinations/one_signal/transform.js +++ b/src/v0/destinations/one_signal/transform.js @@ -122,7 +122,7 @@ const trackResponseBuilder = (message, { Config }) => { if (!externalUserId) { throw new InstrumentationError('userId is required for track events/updating a device'); } - endpoint = `${endpoint}/${appId}/users/${externalUserId}`; + endpoint = `${endpoint}/${appId}/users/${encodeURIComponent(externalUserId)}`; const payload = {}; const tags = {}; /* Populating event as true in tags. @@ -163,7 +163,7 @@ const groupResponseBuilder = (message, { Config }) => { if (!externalUserId) { throw new InstrumentationError('userId is required for group events'); } - endpoint = `${endpoint}/${appId}/users/${externalUserId}`; + endpoint = `${endpoint}/${appId}/users/${encodeURIComponent(externalUserId)}`; const payload = {}; const tags = { groupId, diff --git a/src/v0/util/googleUtils/index.js b/src/v0/util/googleUtils/index.js index c8d872e90e..de73b0fb05 100644 --- a/src/v0/util/googleUtils/index.js +++ b/src/v0/util/googleUtils/index.js @@ -8,21 +8,27 @@ const GOOGLE_ALLOWED_CONSENT_STATUS = ['UNSPECIFIED', 'UNKNOWN', 'GRANTED', 'DEN * ref : https://developers.google.com/google-ads/api/rest/reference/rest/v15/Consent */ -const populateConsentForGoogleDestinations = (properties) => { +const populateConsentForGoogleDestinations = (config) => { const consent = {}; - if ( - properties?.userDataConsent && - GOOGLE_ALLOWED_CONSENT_STATUS.includes(properties.userDataConsent) - ) { - consent.adUserData = properties.userDataConsent; + if (config?.userDataConsent) { + if (GOOGLE_ALLOWED_CONSENT_STATUS.includes(config.userDataConsent)) { + consent.adUserData = config.userDataConsent; + } else { + consent.adUserData = 'UNKNOWN'; + } + } else { + consent.adUserData = 'UNSPECIFIED'; } - if ( - properties?.personalizationConsent && - GOOGLE_ALLOWED_CONSENT_STATUS.includes(properties.personalizationConsent) - ) { - consent.adPersonalization = properties.personalizationConsent; + if (config?.personalizationConsent) { + if (GOOGLE_ALLOWED_CONSENT_STATUS.includes(config.personalizationConsent)) { + consent.adPersonalization = config.personalizationConsent; + } else { + consent.adPersonalization = 'UNKNOWN'; + } + } else { + consent.adPersonalization = 'UNSPECIFIED'; } return consent; }; diff --git a/src/v0/util/googleUtils/index.test.js b/src/v0/util/googleUtils/index.test.js index 27eff2a793..9d1aa5e51a 100644 --- a/src/v0/util/googleUtils/index.test.js +++ b/src/v0/util/googleUtils/index.test.js @@ -1,50 +1,58 @@ const { populateConsentForGoogleDestinations } = require('./index'); describe('unit test for populateConsentForGoogleDestinations', () => { - // Returns an empty object when no properties are provided. - it('should return an empty object when no properties are provided', () => { + it('should return an UNSPECIFIED object when no properties are provided', () => { const result = populateConsentForGoogleDestinations({}); - expect(result).toEqual({}); + expect(result).toEqual({ + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }); }); - // Sets adUserData property of consent object when userDataConsent property is provided and its value is one of the allowed consent statuses. it('should set adUserData property of consent object when userDataConsent property is provided and its value is one of the allowed consent statuses', () => { const properties = { userDataConsent: 'GRANTED' }; const result = populateConsentForGoogleDestinations(properties); - expect(result).toEqual({ adUserData: 'GRANTED' }); + expect(result).toEqual({ adUserData: 'GRANTED', adPersonalization: 'UNSPECIFIED' }); }); - // Sets adPersonalization property of consent object when personalizationConsent property is provided and its value is one of the allowed consent statuses. it('should set adPersonalization property of consent object when personalizationConsent property is provided and its value is one of the allowed consent statuses', () => { const properties = { personalizationConsent: 'DENIED' }; const result = populateConsentForGoogleDestinations(properties); - expect(result).toEqual({ adPersonalization: 'DENIED' }); + expect(result).toEqual({ adPersonalization: 'DENIED', adUserData: 'UNSPECIFIED' }); }); - // Returns an empty object when properties parameter is not provided. - it('should return an empty object when properties parameter is not provided', () => { + it('should return an UNSPECIFIED object when properties parameter is not provided', () => { const result = populateConsentForGoogleDestinations(); - expect(result).toEqual({}); + expect(result).toEqual({ + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }); }); - // Returns an empty object when properties parameter is null. - it('should return an empty object when properties parameter is null', () => { + it('should return an UNSPECIFIED object when properties parameter is null', () => { const result = populateConsentForGoogleDestinations(null); - expect(result).toEqual({}); + expect(result).toEqual({ + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }); }); - // Returns an empty object when properties parameter is an empty object. - it('should return an empty object when properties parameter is an empty object', () => { + it('should return an UNSPECIFIED object when properties parameter is an UNSPECIFIED object', () => { const result = populateConsentForGoogleDestinations({}); - expect(result).toEqual({}); + expect(result).toEqual({ + adPersonalization: 'UNSPECIFIED', + adUserData: 'UNSPECIFIED', + }); }); - // Returns an empty object when properties parameter is an empty object. - it('should return an empty object when properties parameter contains adUserData and adPersonalization with non-allowed values', () => { + it('should return UNKNOWN when properties parameter contains adUserData and adPersonalization with non-allowed values', () => { const result = populateConsentForGoogleDestinations({ - adUserData: 'RANDOM', + userDataConsent: 'RANDOM', personalizationConsent: 'RANDOM', }); - expect(result).toEqual({}); + expect(result).toEqual({ + adPersonalization: 'UNKNOWN', + adUserData: 'UNKNOWN', + }); }); }); diff --git a/src/v0/util/index.js b/src/v0/util/index.js index 9792401241..c1debce088 100644 --- a/src/v0/util/index.js +++ b/src/v0/util/index.js @@ -2201,6 +2201,25 @@ const combineBatchRequestsWithSameJobIds = (inputBatches) => { return combineBatches(combineBatches(inputBatches)); }; +/** + * This function validates the event and return it as string. + * @param {*} isMandatory The event is a required field. + * @param {*} convertToLowerCase The event should be converted to lower-case. + * @returns {string} Event name converted to string. + */ +const validateEventAndLowerCaseConversion = (event, isMandatory, convertToLowerCase) => { + if (!isDefined(event) || typeof event === 'object' || typeof event === 'boolean') { + throw new InstrumentationError('Event should not be a object, NaN, boolean or undefined'); + } + + // handling 0 as it is a valid value + if (isMandatory && !event && event !== 0) { + throw new InstrumentationError('Event is a required field'); + } + + return convertToLowerCase ? event.toString().toLowerCase() : event.toString(); +}; + // ======================================================================== // EXPORTS // ======================================================================== @@ -2317,4 +2336,5 @@ module.exports = { findExistingBatch, removeDuplicateMetadata, combineBatchRequestsWithSameJobIds, + validateEventAndLowerCaseConversion, }; diff --git a/src/v0/util/index.test.js b/src/v0/util/index.test.js index 4dc6255691..810eb5a9d4 100644 --- a/src/v0/util/index.test.js +++ b/src/v0/util/index.test.js @@ -1,4 +1,4 @@ -const { TAG_NAMES } = require('@rudderstack/integrations-lib'); +const { TAG_NAMES, InstrumentationError } = require('@rudderstack/integrations-lib'); const utilities = require('.'); const { getFuncTestData } = require('../../../test/testHelper'); const { FilteredEventsError } = require('./errorTypes'); @@ -7,6 +7,7 @@ const { flattenJson, generateExclusionList, combineBatchRequestsWithSameJobIds, + validateEventAndLowerCaseConversion, } = require('./index'); // Names of the utility functions to test @@ -36,7 +37,6 @@ describe('Utility Functions Tests', () => { test.each(funcTestData)('$description', async ({ description, input, output }) => { try { let result; - // This is to allow sending multiple arguments to the function if (Array.isArray(input)) { result = utilities[funcName](...input); @@ -456,3 +456,53 @@ describe('Unit test cases for combineBatchRequestsWithSameJobIds', () => { expect(combineBatchRequestsWithSameJobIds(input)).toEqual(expectedOutput); }); }); + +describe('validateEventAndLowerCaseConversion Tests', () => { + it('should return string conversion of number types', () => { + const ev = 0; + expect(validateEventAndLowerCaseConversion(ev, false, true)).toBe('0'); + expect(validateEventAndLowerCaseConversion(ev, true, false)).toBe('0'); + }); + + it('should convert string types to lowercase', () => { + const ev = 'Abc'; + expect(validateEventAndLowerCaseConversion(ev, true, true)).toBe('abc'); + }); + + it('should throw error if event is object type', () => { + expect(() => { + validateEventAndLowerCaseConversion({}, true, true); + }).toThrow(InstrumentationError); + expect(() => { + validateEventAndLowerCaseConversion([1, 2], false, true); + }).toThrow(InstrumentationError); + expect(() => { + validateEventAndLowerCaseConversion({ a: 1 }, true, true); + }).toThrow(InstrumentationError); + }); + + it('should convert string to lowercase', () => { + expect(validateEventAndLowerCaseConversion('Abc', true, true)).toBe('abc'); + expect(validateEventAndLowerCaseConversion('ABC', true, false)).toBe('ABC'); + expect(validateEventAndLowerCaseConversion('abc55', false, true)).toBe('abc55'); + expect(validateEventAndLowerCaseConversion(123, false, true)).toBe('123'); + }); + + it('should throw error for null and undefined', () => { + expect(() => { + validateEventAndLowerCaseConversion(null, true, true); + }).toThrow(InstrumentationError); + expect(() => { + validateEventAndLowerCaseConversion(undefined, false, true); + }).toThrow(InstrumentationError); + }); + + it('should throw error for boolean values', () => { + expect(() => { + validateEventAndLowerCaseConversion(true, true, true); + }).toThrow(InstrumentationError); + expect(() => { + validateEventAndLowerCaseConversion(false, false, false); + }).toThrow(InstrumentationError); + }); +}); diff --git a/test/integrations/destinations/facebook_conversions/processor/data.ts b/test/integrations/destinations/facebook_conversions/processor/data.ts index beb7eb32aa..6eb90942a7 100644 --- a/test/integrations/destinations/facebook_conversions/processor/data.ts +++ b/test/integrations/destinations/facebook_conversions/processor/data.ts @@ -1434,4 +1434,104 @@ export const data = [ }, mockFns: defaultMockFns, }, + { + name: 'facebook_conversions', + description: 'Track event with standard event order completed with content_type in properties', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + channel: 'web', + context: { + traits: { + email: ' aBc@gmail.com ', + address: { + zip: 1234, + }, + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + }, + }, + event: 'order completed', + integrations: { + All: true, + }, + message_id: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + properties: { + content_type: 'product_group', + revenue: 400, + additional_bet_index: 0, + products: [ + { + product_id: 1234, + quantity: 5, + price: 55, + }, + ], + }, + timestamp: '2023-11-12T15:46:51.693229+05:30', + type: 'track', + }, + destination: { + Config: { + limitedDataUsage: true, + blacklistPiiProperties: [ + { + blacklistPiiProperties: '', + blacklistPiiHash: false, + }, + ], + accessToken: '09876', + datasetId: 'dummyID', + eventsToEvents: [ + { + from: '', + to: '', + }, + ], + removeExternalId: true, + actionSource: 'website', + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://graph.facebook.com/v18.0/dummyID/events?access_token=09876', + headers: {}, + params: {}, + body: { + JSON: {}, + XML: {}, + JSON_ARRAY: {}, + FORM: { + data: [ + '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"Purchase","event_time":1699784211,"action_source":"website","custom_data":{"content_type":"product_group","revenue":400,"additional_bet_index":0,"products":[{"product_id":1234,"quantity":5,"price":55}],"content_ids":[1234],"contents":[{"id":1234,"quantity":5,"item_price":55}],"currency":"USD","value":400,"num_items":1}}', + ], + }, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, ]; diff --git a/test/integrations/destinations/facebook_pixel/processor/data.ts b/test/integrations/destinations/facebook_pixel/processor/data.ts index 557bc7066c..f6a5cd1e20 100644 --- a/test/integrations/destinations/facebook_pixel/processor/data.ts +++ b/test/integrations/destinations/facebook_pixel/processor/data.ts @@ -6460,4 +6460,111 @@ export const data = [ }, }, }, + { + name: 'facebook_pixel', + description: + 'Test 51: properties.content_type is given priority over populating it from categoryToContent mapping.', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + type: 'track', + messageId: 'ec5481b6-a926-4d2e-b293-0b3a77c4d3be', + originalTimestamp: '2023-10-14T15:46:51.693229+05:30', + anonymousId: '00000000000000000000000000', + userId: '12345', + event: 'order completed', + properties: { + content_type: 'product_group', + category: ['clothing', 'fishing'], + order_id: 'rudderstackorder1', + revenue: 12.24, + currency: 'INR', + products: [ + { + quantity: 1, + price: 24.75, + name: 'my product', + sku: 'p-298', + }, + { + quantity: 3, + price: 24.75, + name: 'other product', + sku: 'p-299', + }, + ], + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T11:15:53.296Z', + }, + destination: { + Config: { + blacklistPiiProperties: [ + { + blacklistPiiProperties: '', + blacklistPiiHash: true, + }, + ], + categoryToContent: [ + { + from: 'clothing', + to: 'product', + }, + ], + accessToken: '09876', + pixelId: 'dummyPixelId', + eventsToEvents: [ + { + from: '', + to: '', + }, + ], + valueFieldIdentifier: 'properties.price', + advancedMapping: false, + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: `https://graph.facebook.com/${VERSION}/dummyPixelId/events?access_token=09876`, + headers: {}, + params: {}, + body: { + JSON: {}, + JSON_ARRAY: {}, + XML: {}, + FORM: { + data: [ + '{"user_data":{"external_id":"5994471abb01112afcc18159f6cc74b4f511b99806da59b3caf5a9c173cacfc5"},"event_name":"Purchase","event_time":1697278611,"event_id":"ec5481b6-a926-4d2e-b293-0b3a77c4d3be","action_source":"website","custom_data":{"content_type":"product_group","category[0]":"clothing","category[1]":"fishing","order_id":"rudderstackorder1","revenue":12.24,"currency":"INR","products[0].quantity":1,"products[0].price":24.75,"products[0].name":"my product","products[0].sku":"p-298","products[1].quantity":3,"products[1].price":24.75,"products[1].name":"other product","products[1].sku":"p-299","content_category":"clothing,fishing","content_ids":["p-298","p-299"],"value":12.24,"contents":[{"id":"p-298","quantity":1,"item_price":24.75},{"id":"p-299","quantity":3,"item_price":24.75}],"num_items":2}}', + ], + }, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ].map((d) => ({ ...d, mockFns })); diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/dataDelivery/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/dataDelivery/data.ts index 414e46ea19..fe16ffef47 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/dataDelivery/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/dataDelivery/data.ts @@ -21,7 +21,7 @@ export const data = [ destination: 'google_adwords_remarketing_lists', listId: '709078448', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -101,7 +101,7 @@ export const data = [ listId: '709078448', customerId: '7693729833', destination: 'google_adwords_remarketing_lists', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -200,7 +200,7 @@ export const data = [ listId: '709078448', customerId: '7693729833', destination: 'google_adwords_remarketing_lists', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/network.ts b/test/integrations/destinations/google_adwords_remarketing_lists/network.ts index 8e1edd21aa..8e7c0acbcf 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/network.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/network.ts @@ -7,7 +7,10 @@ export const networkCallsData = [ data: { job: { type: 'CUSTOMER_MATCH_USER_LIST', - customerMatchUserListMetadata: { userList: 'customers/7693729833/userLists/709078448' }, + customerMatchUserListMetadata: { + userList: 'customers/7693729833/userLists/709078448', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, }, }, headers: { @@ -86,7 +89,10 @@ export const networkCallsData = [ data: { job: { type: 'CUSTOMER_MATCH_USER_LIST', - customerMatchUserListMetadata: { userList: 'customers/7693729833/userLists/709078448' }, + customerMatchUserListMetadata: { + userList: 'customers/7693729833/userLists/709078448', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, }, }, headers: { diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts index 804efec220..a846e0370d 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/processor/data.ts @@ -79,7 +79,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -213,7 +213,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -332,7 +332,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -1434,7 +1434,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -2829,7 +2829,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -2909,7 +2909,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -4122,7 +4122,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -5422,7 +5422,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -6799,7 +6799,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -8109,7 +8109,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -9409,7 +9409,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -10804,7 +10804,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -10884,7 +10884,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11059,7 +11059,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11137,7 +11137,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11281,7 +11281,7 @@ export const data = [ params: { listId: 'list111', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11488,7 +11488,7 @@ export const data = [ params: { listId: 'aud1234', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11627,7 +11627,7 @@ export const data = [ params: { listId: '830441345', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11765,7 +11765,7 @@ export const data = [ params: { listId: '830441345', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, }, body: { JSON: { @@ -11836,6 +11836,8 @@ export const data = [ userSchema: ['email', 'phone', 'addressInfo'], isHashRequired: true, typeOfList: 'General', + userDataConsent: 'UNSPECIFIED', + personalizationConsent: 'GRANTED', }, }, message: { @@ -11860,8 +11862,6 @@ export const data = [ event: 'Add_Audience', messageId: 'bd2d67ca-0c9a-4d3b-a2f8-35a3c3f75ba7', properties: { - userDataConsent: 'UNSPECIFIED', - personalizationConsent: 'GRANTED', listData: { add: [ { @@ -11979,6 +11979,8 @@ export const data = [ userSchema: ['email', 'phone', 'addressInfo'], isHashRequired: true, typeOfList: 'General', + userDataConsent: 'RANDOM', + personalizationConsent: 'RANDOM', }, }, message: { @@ -12003,8 +12005,6 @@ export const data = [ event: 'Add_Audience', messageId: 'bd2d67ca-0c9a-4d3b-a2f8-35a3c3f75ba7', properties: { - userDataConsent: 'RANDOM', - personalizationConsent: 'RANDOM', listData: { add: [ { @@ -12048,7 +12048,7 @@ export const data = [ params: { listId: '830441345', customerId: '7693729833', - consent: {}, + consent: { adPersonalization: 'UNKNOWN', adUserData: 'UNKNOWN' }, }, body: { JSON: { diff --git a/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts b/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts index 8c12225400..31d5c72694 100644 --- a/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts +++ b/test/integrations/destinations/google_adwords_remarketing_lists/router/data.ts @@ -228,7 +228,11 @@ export const data = [ 'Content-Type': 'application/json', 'developer-token': 'ijkl9101', }, - params: { listId: '7090784486', customerId: '7693729833', consent: {} }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, body: { JSON: { enablePartialFailure: true, @@ -305,7 +309,11 @@ export const data = [ 'Content-Type': 'application/json', 'developer-token': 'ijkl9101', }, - params: { listId: '7090784486', customerId: '7693729833', consent: {} }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, body: { JSON: { enablePartialFailure: true, @@ -359,7 +367,11 @@ export const data = [ 'Content-Type': 'application/json', 'developer-token': 'ijkl9101', }, - params: { listId: '7090784486', customerId: '7693729833', consent: {} }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, body: { JSON: { enablePartialFailure: true, @@ -436,7 +448,11 @@ export const data = [ 'Content-Type': 'application/json', 'developer-token': 'ijkl9101', }, - params: { listId: '7090784486', customerId: '7693729833', consent: {} }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, body: { JSON: { enablePartialFailure: true, @@ -484,7 +500,11 @@ export const data = [ 'Content-Type': 'application/json', 'developer-token': 'ijkl9101', }, - params: { listId: '7090784486', customerId: '7693729833', consent: {} }, + params: { + listId: '7090784486', + customerId: '7693729833', + consent: { adPersonalization: 'UNSPECIFIED', adUserData: 'UNSPECIFIED' }, + }, body: { JSON: { enablePartialFailure: true, diff --git a/test/integrations/destinations/klaviyo/processor/identifyTestData.ts b/test/integrations/destinations/klaviyo/processor/identifyTestData.ts index f632cb767c..0dd4751133 100644 --- a/test/integrations/destinations/klaviyo/processor/identifyTestData.ts +++ b/test/integrations/destinations/klaviyo/processor/identifyTestData.ts @@ -47,6 +47,8 @@ const commonTraits = { }, }; +const commonTraits2 = { ...commonTraits, street: '63, Shibuya' }; + const commonOutputUserProps = { external_id: 'user@1', email: 'test@rudderstack.com', @@ -67,6 +69,12 @@ const commonOutputUserProps = { }, }; +const commonOutputUserProps2 = { + ...commonOutputUserProps, + location: { ...commonOutputUserProps.location, address1: '63, Shibuya' }, + properties: { ...commonOutputUserProps.properties, street: '63, Shibuya' }, +}; + const commonOutputSubscriptionProps = { list_id: 'XUepkK', subscriptions: [ @@ -116,7 +124,7 @@ export const identifyData: ProcessorTestData[] = [ destination, message: generateSimplifiedIdentifyPayload({ context: { - traits: commonTraits, + traits: commonTraits2, }, anonymousId, userId, @@ -140,7 +148,7 @@ export const identifyData: ProcessorTestData[] = [ JSON: { data: { type: 'profile', - attributes: commonOutputUserProps, + attributes: commonOutputUserProps2, id: '01GW3PHVY0MTCDGS0A1612HARX', }, }, @@ -188,7 +196,7 @@ export const identifyData: ProcessorTestData[] = [ userId, context: { traits: { - ...commonTraits, + ...commonTraits2, friend: { names: { first: 'Alice', @@ -221,9 +229,9 @@ export const identifyData: ProcessorTestData[] = [ type: 'profile', id: '01GW3PHVY0MTCDGS0A1612HARX', attributes: { - ...commonOutputUserProps, + ...commonOutputUserProps2, properties: { - ...commonOutputUserProps.properties, + ...commonOutputUserProps2.properties, 'friend.age': 25, 'friend.names.first': 'Alice', 'friend.names.last': 'Smith', @@ -278,7 +286,7 @@ export const identifyData: ProcessorTestData[] = [ userId, context: { traits: { - ...commonTraits, + ...commonTraits2, email: 'test3@rudderstack.com', }, }, @@ -334,7 +342,7 @@ export const identifyData: ProcessorTestData[] = [ userId, context: { traits: { - ...commonTraits, + ...commonTraits2, properties: { ...commonTraits.properties, subscribe: false }, }, }, @@ -358,7 +366,7 @@ export const identifyData: ProcessorTestData[] = [ JSON: { data: { type: 'profile', - attributes: commonOutputUserProps, + attributes: commonOutputUserProps2, id: '01GW3PHVY0MTCDGS0A1612HARX', }, }, @@ -390,7 +398,7 @@ export const identifyData: ProcessorTestData[] = [ sentAt, userId, context: { - traits: commonTraits, + traits: commonTraits2, }, anonymousId, originalTimestamp, @@ -414,9 +422,9 @@ export const identifyData: ProcessorTestData[] = [ data: { type: 'profile', attributes: removeUndefinedAndNullValues({ - ...commonOutputUserProps, + ...commonOutputUserProps2, properties: { - ...commonOutputUserProps.properties, + ...commonOutputUserProps2.properties, _id: userId, }, // remove external_id from the payload @@ -546,7 +554,7 @@ export const identifyData: ProcessorTestData[] = [ userId, context: { traits: removeUndefinedAndNullValues({ - ...commonTraits, + ...commonTraits2, email: undefined, phone: undefined, }), diff --git a/test/integrations/destinations/mp/processor/data.ts b/test/integrations/destinations/mp/processor/data.ts index dfa94352c9..5b2d0fbfff 100644 --- a/test/integrations/destinations/mp/processor/data.ts +++ b/test/integrations/destinations/mp/processor/data.ts @@ -121,7 +121,10 @@ export const data = [ request: { body: [ { - destination: sampleDestination, + destination: overrideDestination(sampleDestination, { + useUserDefinedPageEventName: true, + userDefinedPageEventTemplate: 'Viewed a {{ name }} page', + }), message: { anonymousId: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', channel: 'web', @@ -195,7 +198,7 @@ export const data = [ JSON: {}, JSON_ARRAY: { batch: - '[{"event":"Loaded a Page","properties":{"ip":"0.0.0.0","$user_id":"hjikl","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"dd266c67-9199-4a52-ba32-f46ddde67312","token":"dummyApiKey","distinct_id":"hjikl","time":1579847342402,"name":"Contact Us","category":"Contact","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', + '[{"event":"Viewed a Contact Us page","properties":{"ip":"0.0.0.0","$user_id":"hjikl","$current_url":"https://docs.rudderstack.com/destinations/mixpanel","$screen_dpi":2,"mp_lib":"RudderLabs JavaScript SDK","$app_build_number":"1.0.0","$app_version_string":"1.0.5","$insert_id":"dd266c67-9199-4a52-ba32-f46ddde67312","token":"dummyApiKey","distinct_id":"hjikl","time":1579847342402,"name":"Contact Us","category":"Contact","$browser":"Chrome","$browser_version":"79.0.3945.117"}}]', }, XML: {}, FORM: {}, diff --git a/test/integrations/destinations/ninetailed/commonConfig.ts b/test/integrations/destinations/ninetailed/commonConfig.ts new file mode 100644 index 0000000000..3b5d4149f2 --- /dev/null +++ b/test/integrations/destinations/ninetailed/commonConfig.ts @@ -0,0 +1,112 @@ +export const destination = { + ID: 'random_id', + Name: 'ninetailed', + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + organisationId: 'dummyOrganisationId', + environment: 'main', + }, +}; + +export const metadata = { + destinationId: 'dummyDestId', +}; +export const commonProperties = { + segment: 'SampleSegment', + shipcountry: 'USA', + shipped: '20240129_1500', + sitename: 'SampleSiteName', + storeId: '12345', + storecat: 'Electronics', +}; +export const traits = { + email: 'test@user.com', + firstname: 'John', + lastname: 'Doe', + phone: '+1(123)456-7890', + gender: 'Male', + birthday: '1980-01-02', + city: 'San Francisco', +}; +export const context = { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + campaign: { + name: 'campign_123', + source: 'social marketing', + medium: 'facebook', + term: '1 year', + }, + library: { + name: 'RudderstackSDK', + version: 'Ruddderstack SDK version', + }, + locale: 'en-US', + page: { + path: '/signup', + referrer: 'https://rudderstack.medium.com/', + search: '?type=freetrial', + url: 'https://app.rudderstack.com/signup?type=freetrial', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + location: { + coordinates: { + latitude: 40.7128, + longitude: -74.006, + }, + city: 'San Francisco', + postalCode: '94107', + region: 'CA', + regionCode: 'CA', + country: ' United States', + countryCode: 'United States of America', + continent: 'North America', + timezone: 'America/Los_Angeles', + }, +}; + +export const commonInput = { + anonymousId: 'anon_123', + messageId: 'dummy_msg_id', + context, + channel: 'web', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', +}; + +export const commonOutput = { + anonymousId: 'anon_123', + messageId: 'dummy_msg_id', + context, + channel: 'web', + originalTimestamp: '2021-01-25T15:32:56.409Z', +}; + +export const endpoint = + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events'; +export const routerInstrumentationErrorStatTags = { + destType: 'NINETAILED', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', +}; +export const processInstrumentationErrorStatTags = { + destType: 'NINETAILED', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + destinationId: 'dummyDestId', +}; diff --git a/test/integrations/destinations/ninetailed/mocks.ts b/test/integrations/destinations/ninetailed/mocks.ts new file mode 100644 index 0000000000..a16b276053 --- /dev/null +++ b/test/integrations/destinations/ninetailed/mocks.ts @@ -0,0 +1,5 @@ +import config from '../../../../src/cdk/v2/destinations/ninetailed/config'; + +export const defaultMockFns = () => { + jest.replaceProperty(config, 'MAX_BATCH_SIZE', 2); +}; diff --git a/test/integrations/destinations/ninetailed/processor/data.ts b/test/integrations/destinations/ninetailed/processor/data.ts new file mode 100644 index 0000000000..4e5fa72365 --- /dev/null +++ b/test/integrations/destinations/ninetailed/processor/data.ts @@ -0,0 +1,5 @@ +import { validationFailures } from './validation'; +import { track } from './track'; +import { page } from './page'; +import { identify } from './identify'; +export const data = [...identify, ...page, ...track, ...validationFailures]; diff --git a/test/integrations/destinations/ninetailed/processor/identify.ts b/test/integrations/destinations/ninetailed/processor/identify.ts new file mode 100644 index 0000000000..fbd7379e19 --- /dev/null +++ b/test/integrations/destinations/ninetailed/processor/identify.ts @@ -0,0 +1,155 @@ +import { + destination, + traits, + commonInput, + metadata, + processInstrumentationErrorStatTags, +} from '../commonConfig'; +import { transformResultBuilder } from '../../../testUtils'; +export const identify = [ + { + id: 'ninetailed-test-identify-success-1', + name: 'ninetailed', + description: 'identify call with all mappings available', + scenario: 'Framework+Buisness', + successCriteria: 'Response should contain all the mappings and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + ...commonInput, + userId: 'sajal12', + traits: traits, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + destinationId: 'dummyDestId', + }, + output: transformResultBuilder({ + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + JSON: { + events: [ + { + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + campaign: { + name: 'campign_123', + source: 'social marketing', + medium: 'facebook', + term: '1 year', + }, + library: { + name: 'RudderstackSDK', + version: 'Ruddderstack SDK version', + }, + locale: 'en-US', + page: { + path: '/signup', + referrer: 'https://rudderstack.medium.com/', + search: '?type=freetrial', + url: 'https://app.rudderstack.com/signup?type=freetrial', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + location: { + coordinates: { + latitude: 40.7128, + longitude: -74.006, + }, + city: 'San Francisco', + postalCode: '94107', + region: 'CA', + regionCode: 'CA', + country: ' United States', + countryCode: 'United States of America', + continent: 'North America', + timezone: 'America/Los_Angeles', + }, + }, + type: 'identify', + channel: 'web', + userId: 'sajal12', + messageId: 'dummy_msg_id', + traits: traits, + anonymousId: 'anon_123', + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + ], + }, + userId: '', + }), + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'ninetailed-test-identify-failure-1', + name: 'ninetailed', + description: 'identify call with no userId available', + scenario: 'Framework', + successCriteria: + 'Error should be thrown for required field userId not present and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + ...commonInput, + type: 'identify', + channel: 'mobile', + messageId: 'dummy_msg_id', + traits: traits, + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Missing required value from "userIdOnly": Workflow: procWorkflow, Step: preparePayload, ChildStep: undefined, OriginalError: Missing required value from "userIdOnly"', + metadata: { + destinationId: 'dummyDestId', + }, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/ninetailed/processor/page.ts b/test/integrations/destinations/ninetailed/processor/page.ts new file mode 100644 index 0000000000..93a086ceea --- /dev/null +++ b/test/integrations/destinations/ninetailed/processor/page.ts @@ -0,0 +1,108 @@ +import { destination, context, commonProperties, metadata } from '../commonConfig'; +import { transformResultBuilder } from '../../../testUtils'; +export const page = [ + { + id: 'ninetailed-test-page-success-1', + name: 'ninetailed', + description: 'page call with all mappings available', + scenario: 'Framework+Buisness', + successCriteria: 'Response should contain all the mappings and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context, + type: 'page', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + destinationId: 'dummyDestId', + }, + output: transformResultBuilder({ + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + JSON: { + events: [ + { + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + campaign: { + name: 'campign_123', + source: 'social marketing', + medium: 'facebook', + term: '1 year', + }, + library: { + name: 'RudderstackSDK', + version: 'Ruddderstack SDK version', + }, + locale: 'en-US', + page: { + path: '/signup', + referrer: 'https://rudderstack.medium.com/', + search: '?type=freetrial', + url: 'https://app.rudderstack.com/signup?type=freetrial', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + location: { + coordinates: { + latitude: 40.7128, + longitude: -74.006, + }, + city: 'San Francisco', + postalCode: '94107', + region: 'CA', + regionCode: 'CA', + country: ' United States', + countryCode: 'United States of America', + continent: 'North America', + timezone: 'America/Los_Angeles', + }, + }, + type: 'page', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + ], + }, + userId: '', + }), + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/ninetailed/processor/track.ts b/test/integrations/destinations/ninetailed/processor/track.ts new file mode 100644 index 0000000000..6b6a1e7831 --- /dev/null +++ b/test/integrations/destinations/ninetailed/processor/track.ts @@ -0,0 +1,204 @@ +import { destination, context, commonProperties, metadata } from '../commonConfig'; +import { transformResultBuilder } from '../../../testUtils'; +export const track = [ + { + id: 'ninetailed-test-track-success-1', + name: 'ninetailed', + description: 'Track call with all mappings available', + scenario: 'Framework+Buisness', + successCriteria: 'Response should contain all the mappings and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + campaign: { + name: 'campign_123', + source: 'social marketing', + medium: 'facebook', + term: '1 year', + }, + library: { + name: 'RudderstackSDK', + version: 'Ruddderstack SDK version', + }, + locale: 'en-US', + page: { + path: '/signup', + referrer: 'https://rudderstack.medium.com/', + search: '?type=freetrial', + url: 'https://app.rudderstack.com/signup?type=freetrial', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + location: { + coordinates: { + latitude: 40.7128, + longitude: -74.006, + }, + city: 'San Francisco', + postalCode: '94107', + region: 'CA', + regionCode: 'CA', + country: ' United States', + countryCode: 'United States of America', + continent: 'North America', + timezone: 'America/Los_Angeles', + }, + }, + type: 'track', + event: 'product purchased', + userId: 'sajal12', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + destinationId: 'dummyDestId', + }, + output: transformResultBuilder({ + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + JSON: { + events: [ + { + context: { + app: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + campaign: { + name: 'campign_123', + source: 'social marketing', + medium: 'facebook', + term: '1 year', + }, + library: { + name: 'RudderstackSDK', + version: 'Ruddderstack SDK version', + }, + locale: 'en-US', + page: { + path: '/signup', + referrer: 'https://rudderstack.medium.com/', + search: '?type=freetrial', + url: 'https://app.rudderstack.com/signup?type=freetrial', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + location: { + coordinates: { + latitude: 40.7128, + longitude: -74.006, + }, + city: 'San Francisco', + postalCode: '94107', + region: 'CA', + regionCode: 'CA', + country: ' United States', + countryCode: 'United States of America', + continent: 'North America', + timezone: 'America/Los_Angeles', + }, + }, + type: 'track', + event: 'product purchased', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + ], + }, + userId: '', + }), + statusCode: 200, + }, + ], + }, + }, + }, + { + id: 'ninetailed-test-track-failure-1', + name: 'ninetailed', + description: 'track call with no event available', + scenario: 'Framework', + successCriteria: + 'Error should be thrown for required field event not present and status code should be 200', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + context, + type: 'track', + channel: 'mobile', + messageId: 'dummy_msg_id', + properties: commonProperties, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Missing required value from "event": Workflow: procWorkflow, Step: preparePayload, ChildStep: undefined, OriginalError: Missing required value from "event"', + metadata: { + destinationId: 'dummyDestId', + }, + statTags: { + destType: 'NINETAILED', + destinationId: 'dummyDestId', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + }, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/ninetailed/processor/validation.ts b/test/integrations/destinations/ninetailed/processor/validation.ts new file mode 100644 index 0000000000..68c025faad --- /dev/null +++ b/test/integrations/destinations/ninetailed/processor/validation.ts @@ -0,0 +1,115 @@ +import { processInstrumentationErrorStatTags, destination, context } from '../commonConfig'; + +export const validationFailures = [ + { + id: 'Ninetailed-validation-test-1', + name: 'ninetailed', + description: 'Required field anonymousId not present', + scenario: 'Framework', + successCriteria: 'Transformationn Error for anonymousId not present', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + event: 'product purchased', + sentAt: '2021-01-25T16:12:02.048Z', + userId: 'sajal12', + context, + properties: { + products: [{}], + }, + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Missing required value from "anonymousId": Workflow: procWorkflow, Step: preparePayload, ChildStep: undefined, OriginalError: Missing required value from "anonymousId"', + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'Ninetailed-test-4', + name: 'ninetailed', + description: 'Unsupported message type -> group', + scenario: 'Framework', + successCriteria: 'Transformationn Error for Unsupported message type', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'group', + sentAt: '2021-01-25T16:12:02.048Z', + userId: 'sajal12', + channel: 'mobile', + rudderId: 'b7b24f86-f7bf-46d8-b2b4-ccafc080239c', + messageId: 'dummy_msg_id', + traits: { + orderId: 'ord 123', + products: [], + }, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + originalTimestamp: '2021-01-25T15:32:56.409Z', + }, + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'message type group is not supported: Workflow: procWorkflow, Step: validateInput, ChildStep: undefined, OriginalError: message type group is not supported', + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/ninetailed/router/basicProperties.ts b/test/integrations/destinations/ninetailed/router/basicProperties.ts new file mode 100644 index 0000000000..5725f3d445 --- /dev/null +++ b/test/integrations/destinations/ninetailed/router/basicProperties.ts @@ -0,0 +1,33 @@ +export const trackProperties = { + index: 'products', + queryId: '43b15df305339e827f0ac0bdc5ebcaa7', + products: [ + { objectId: 'ecommerce-sample-data-919', position: 7 }, + { objectId: '9780439784542', position: 8 }, + ], +}; + +export const pageProperties = { + title: 'Sample Page', + url: 'https://example.com/?utm_campaign=example_campaign&utm_content=example_content', + path: '/', + hash: '', + search: '?utm_campaign=example_campaign&utm_content=example_content', + width: '1920', + height: '1080', + query: { + utm_campaign: 'example_campaign', + utm_content: 'example_content', + }, + referrer: '', +}; + +export const traits = { + email: 'test@user.com', + firstname: 'John', + lastname: 'Doe', + phone: '+1(123)456-7890', + gender: 'Male', + birthday: '1980-01-02', + city: 'San Francisco', +}; diff --git a/test/integrations/destinations/ninetailed/router/data.ts b/test/integrations/destinations/ninetailed/router/data.ts new file mode 100644 index 0000000000..05105f4aed --- /dev/null +++ b/test/integrations/destinations/ninetailed/router/data.ts @@ -0,0 +1,393 @@ +import { + commonInput, + destination, + commonOutput, + routerInstrumentationErrorStatTags, +} from '../commonConfig'; +import { trackProperties, pageProperties, traits } from './basicProperties'; +import { defaultMockFns } from '../mocks'; + +export const data = [ + { + name: 'ninetailed', + id: 'Test 0 - router', + description: 'Batch calls with all three type of calls as success', + scenario: 'Framework+Buisness', + successCriteria: 'All events should be transformed successfully and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + ...commonInput, + type: 'track', + event: 'product list viewed', + properties: trackProperties, + }, + metadata: { jobId: 1, userId: 'u1' }, + destination, + }, + { + message: { + ...commonInput, + type: 'page', + properties: pageProperties, + }, + metadata: { jobId: 2, userId: 'u1' }, + destination, + }, + { + message: { + type: 'identify', + ...commonInput, + userId: 'testuserId1', + traits, + integrations: { All: true }, + }, + metadata: { jobId: 3, userId: 'u1' }, + destination, + }, + ], + destType: 'ninetailed', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + params: {}, + body: { + FORM: {}, + JSON: { + events: [ + { + ...commonOutput, + type: 'track', + event: 'product list viewed', + properties: trackProperties, + }, + { + ...commonOutput, + type: 'page', + properties: pageProperties, + }, + { + type: 'identify', + ...commonOutput, + userId: 'testuserId1', + traits, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + headers: { + 'Content-Type': 'application/json', + }, + files: {}, + }, + metadata: [ + { jobId: 1, userId: 'u1' }, + { jobId: 2, userId: 'u1' }, + { jobId: 3, userId: 'u1' }, + ], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + }, + { + name: 'ninetailed', + id: 'Test 1 - router', + description: 'Batch calls with one fail invalid event and two valid events', + scenario: 'Framework+Buisness', + successCriteria: + 'Two events should be transformed successfully and one should fail and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + ...commonInput, + type: 'track', + event: 'product list viewed', + properties: trackProperties, + }, + metadata: { jobId: 1, userId: 'u1' }, + destination, + }, + { + message: { + ...commonInput, + type: 'page', + properties: { + title: 'Sample Page', + url: 'https://example.com/?utm_campaign=example_campaign&utm_content=example_content', + path: '/', + hash: '', + search: '?utm_campaign=example_campaign&utm_content=example_content', + width: '1920', + height: '1080', + query: { + utm_campaign: 'example_campaign', + utm_content: 'example_content', + }, + referrer: '', + }, + }, + metadata: { jobId: 2, userId: 'u1' }, + destination, + }, + { + message: { + type: 'identify', + ...commonInput, + traits, + integrations: { All: true }, + }, + metadata: { jobId: 3, userId: 'u1' }, + destination, + }, + ], + destType: 'ninetailed', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + destination, + error: 'Missing required value from "userIdOnly"', + metadata: [{ jobId: 3, userId: 'u1' }], + statTags: routerInstrumentationErrorStatTags, + statusCode: 400, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + params: {}, + body: { + FORM: {}, + JSON: { + events: [ + { + ...commonOutput, + type: 'track', + event: 'product list viewed', + properties: trackProperties, + }, + { + ...commonOutput, + type: 'page', + properties: pageProperties, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + headers: { + 'Content-Type': 'application/json', + }, + files: {}, + }, + metadata: [ + { jobId: 1, userId: 'u1' }, + { jobId: 2, userId: 'u1' }, + ], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + name: 'ninetailed', + id: 'Test 2 - router', + description: 'Batch calls with 3 succesfull events and 1 failed event', + scenario: 'Framework+Buisness', + successCriteria: + '3 successful events should be distributed in two and 1 failed in one hence total batches should be 3 and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + ...commonInput, + type: 'track', + event: 'product list viewed', + properties: trackProperties, + }, + metadata: { jobId: 1, userId: 'u1' }, + destination, + }, + { + message: { + ...commonInput, + type: 'page', + properties: pageProperties, + }, + metadata: { jobId: 2, userId: 'u1' }, + destination, + }, + { + message: { + type: 'identify', + ...commonInput, + userId: 'testuserId1', + traits, + integrations: { All: true }, + }, + metadata: { jobId: 3, userId: 'u1' }, + destination, + }, + { + message: { + type: 'identify', + ...commonInput, + traits, + integrations: { All: true }, + }, + metadata: { jobId: 4, userId: 'u1' }, + destination, + }, + ], + destType: 'ninetailed', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + destination, + error: 'Missing required value from "userIdOnly"', + metadata: [{ jobId: 4, userId: 'u1' }], + statTags: routerInstrumentationErrorStatTags, + statusCode: 400, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + params: {}, + body: { + FORM: {}, + JSON: { + events: [ + { + ...commonOutput, + type: 'track', + event: 'product list viewed', + properties: trackProperties, + }, + { + ...commonOutput, + type: 'page', + properties: pageProperties, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + headers: { + 'Content-Type': 'application/json', + }, + files: {}, + }, + metadata: [ + { jobId: 1, userId: 'u1' }, + { jobId: 2, userId: 'u1' }, + ], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: + 'https://experience.ninetailed.co/v2/organizations/dummyOrganisationId/environments/main/events', + params: {}, + body: { + FORM: {}, + JSON: { + events: [ + { + type: 'identify', + ...commonOutput, + userId: 'testuserId1', + traits, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + headers: { + 'Content-Type': 'application/json', + }, + files: {}, + }, + metadata: [{ jobId: 3, userId: 'u1' }], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/one_signal/processor/data.ts b/test/integrations/destinations/one_signal/processor/data.ts index 7f244aa711..4171157aef 100644 --- a/test/integrations/destinations/one_signal/processor/data.ts +++ b/test/integrations/destinations/one_signal/processor/data.ts @@ -702,7 +702,7 @@ export const data = [ headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, version: '1', endpoint: - 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user@27', + 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user%4027', userId: '', }, statusCode: 200, @@ -789,7 +789,7 @@ export const data = [ headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, version: '1', endpoint: - 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user@27', + 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user%4027', userId: '', }, statusCode: 200, @@ -870,7 +870,7 @@ export const data = [ headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, version: '1', endpoint: - 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user@27', + 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user%4027', userId: '', }, statusCode: 200, @@ -945,7 +945,7 @@ export const data = [ headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, version: '1', endpoint: - 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user@27', + 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user%4027', userId: '', }, statusCode: 200, @@ -1025,7 +1025,7 @@ export const data = [ headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, version: '1', endpoint: - 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user@27', + 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user%4027', userId: '', }, statusCode: 200, diff --git a/test/integrations/destinations/one_signal/router/data.ts b/test/integrations/destinations/one_signal/router/data.ts index fe8460e45d..a27da5a745 100644 --- a/test/integrations/destinations/one_signal/router/data.ts +++ b/test/integrations/destinations/one_signal/router/data.ts @@ -199,7 +199,7 @@ export const data = [ headers: { Accept: 'application/json', 'Content-Type': 'application/json' }, version: '1', endpoint: - 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user@27', + 'https://onesignal.com/api/v1/apps/random-818c-4a28-b98e-6cd8a994eb22/users/user%4027', }, metadata: [{ jobId: 2, userId: 'u1' }], batched: false, diff --git a/test/integrations/destinations/reddit/dataDelivery/business.ts b/test/integrations/destinations/reddit/dataDelivery/business.ts new file mode 100644 index 0000000000..2c4714ef13 --- /dev/null +++ b/test/integrations/destinations/reddit/dataDelivery/business.ts @@ -0,0 +1,131 @@ +import { + generateMetadata, + generateProxyV0Payload, + generateProxyV1Payload, +} from '../../../testUtils'; + +const validRequestPayload = { + 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: 3, + products: [ + { + id: '123', + name: 'Monopoly', + category: 'Games', + }, + { + id: '345', + name: 'UNO', + category: 'Games', + }, + ], + }, + }, + ], +}; + +const commonHeaders = { + Authorization: 'Bearer dummyAccessToken', + 'Content-Type': 'application/json', +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: validRequestPayload, +}; + +export const testScenariosForV0API = [ + { + id: 'reddit_v0_scenario_1', + name: 'reddit', + description: + '[Proxy v0 API] :: Test for a valid request with a successful 200 response from the destination', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://ads-api.reddit.com/api/v2.0/conversions/events/a2_fsddXXXfsfd', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + destResp: { + response: { + message: 'Successfully processed 1 conversion events.', + }, + status: 200, + }, + message: 'Request Processed Successfully', + status: 200, + }, + }, + }, + }, + }, +]; + +export const testScenariosForV1API = [ + { + id: 'reddit_v1_scenario_1', + name: 'reddit', + description: + '[Proxy v1 API] :: Test for a valid request with a successful 200 response from the destination', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://dfareporting.googleapis.com/test_url_for_valid_request', + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + message: 'Request Processed Successfully', + response: [ + { + metadata: generateMetadata(1), + statusCode: 500, + }, + ], + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/reddit/dataDelivery/data.ts b/test/integrations/destinations/reddit/dataDelivery/data.ts new file mode 100644 index 0000000000..54728ecb90 --- /dev/null +++ b/test/integrations/destinations/reddit/dataDelivery/data.ts @@ -0,0 +1,9 @@ +import { testScenariosForV0API, testScenariosForV1API } from './business'; +import { v0oauthScenarios, v1oauthScenarios } from './oauth'; + +export const data = [ + ...v0oauthScenarios, + ...v1oauthScenarios, + ...testScenariosForV0API, + ...testScenariosForV1API, +]; diff --git a/test/integrations/destinations/reddit/dataDelivery/oauth.ts b/test/integrations/destinations/reddit/dataDelivery/oauth.ts new file mode 100644 index 0000000000..90368cd60b --- /dev/null +++ b/test/integrations/destinations/reddit/dataDelivery/oauth.ts @@ -0,0 +1,147 @@ +import { + generateMetadata, + generateProxyV1Payload, + generateProxyV0Payload, +} from '../../../testUtils'; + +const authorizationRequiredRequestPayload = { + events: [ + { + event_at: '2019-10-14T09:03:17.562Z', + event_type: { + tracking_type: 'ViewContent', + }, + 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: 3, + products: [ + { + id: '123', + name: 'Monopoly', + category: 'Games', + }, + { + id: '345', + name: 'UNO', + category: 'Games', + }, + ], + }, + }, + ], +}; + +const commonHeaders = { + Authorization: 'Bearer dummyAccessToken', + 'Content-Type': 'application/json', +}; + +const commonRequestParameters = { + headers: commonHeaders, + JSON: authorizationRequiredRequestPayload, +}; + +const expectedStatTags = { + destType: 'REDDIT', + destinationId: 'default-destinationId', + errorCategory: 'network', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'default-workspaceId', +}; + +export const v0oauthScenarios = [ + { + id: 'reddit_v0_oauth_scenario_1', + name: 'reddit', + description: '[Proxy v0 API] :: Oauth where Authorization Required response from destination', + successCriteria: 'Should return 500 with authErrorCategory as REFRESH_TOKEN', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + ...commonRequestParameters, + endpoint: 'https://ads-api.reddit.com/api/v2.0/conversions/events/a2_gsddXXXfsfd', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + authErrorCategory: 'REFRESH_TOKEN', + destinationResponse: { + response: 'Authorization Required', + status: 401, + }, + message: + "Request failed due to Authorization Required 'during reddit response transformation'", + statTags: expectedStatTags, + status: 500, + }, + }, + }, + }, + }, +]; + +export const v1oauthScenarios = [ + { + id: 'reddit_v1_oauth_scenario_1', + name: 'reddit', + description: '[Proxy v1 API] :: Oauth where Authorization Required response from destination', + successCriteria: 'Should return 500 with authErrorCategory as REFRESH_TOKEN', + scenario: 'Oauth', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParameters, + endpoint: 'https://ads-api.reddit.com/api/v2.0/conversions/events/a2_gsddXXXfsfd', + }, + [generateMetadata(1)], + ), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + authErrorCategory: 'REFRESH_TOKEN', + message: + "Request failed due to Authorization Required 'during reddit response transformation'", + response: [ + { + error: '"Authorization Required"', + metadata: generateMetadata(1), + statusCode: 500, + }, + ], + statTags: expectedStatTags, + status: 500, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/reddit/delivery/data.ts b/test/integrations/destinations/reddit/delivery/data.ts deleted file mode 100644 index 66c1e2863f..0000000000 --- a/test/integrations/destinations/reddit/delivery/data.ts +++ /dev/null @@ -1,174 +0,0 @@ -export const data = [ - { - name: 'reddit', - description: 'Test 0', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - 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: 3, - products: [ - { - id: '123', - name: 'Monopoly', - category: 'Games', - }, - { - id: '345', - name: 'UNO', - category: 'Games', - }, - ], - }, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: { - destResp: { - response: { - message: 'Successfully processed 1 conversion events.', - }, - status: 200, - }, - message: 'Request Processed Successfully', - status: 200, - }, - }, - }, - }, - }, - { - name: 'reddit', - description: 'Test 1', - feature: 'dataDelivery', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://ads-api.reddit.com/api/v2.0/conversions/events/a2_gsddXXXfsfd', - headers: { - Authorization: 'Bearer dummyAccessToken', - 'Content-Type': 'application/json', - }, - params: {}, - body: { - JSON: { - events: [ - { - event_at: '2019-10-14T09:03:17.562Z', - event_type: { - tracking_type: 'ViewContent', - }, - 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: 3, - products: [ - { - id: '123', - name: 'Monopoly', - category: 'Games', - }, - { - id: '345', - name: 'UNO', - category: 'Games', - }, - ], - }, - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - userId: '', - }, - method: 'POST', - }, - }, - output: { - response: { - status: 500, - body: { - output: { - authErrorCategory: 'REFRESH_TOKEN', - destinationResponse: { - response: 'Authorization Required', - status: 401, - }, - message: - "Request failed due to Authorization Required 'during reddit response transformation'", - statTags: { - destType: 'REDDIT', - destinationId: 'Non-determininable', - errorCategory: 'network', - errorType: 'retryable', - feature: 'dataDelivery', - implementation: 'native', - module: 'destination', - workspaceId: 'Non-determininable', - }, - status: 500, - }, - }, - }, - }, - }, -];