diff --git a/src/cdk/v2/destinations/algolia/procWorkflow.yaml b/src/cdk/v2/destinations/algolia/procWorkflow.yaml index f9ac8e3ae6..87da64aa45 100644 --- a/src/cdk/v2/destinations/algolia/procWorkflow.yaml +++ b/src/cdk/v2/destinations/algolia/procWorkflow.yaml @@ -5,7 +5,10 @@ bindings: - path: ../../../../v0/destinations/algolia/config - name: removeUndefinedAndNullValues path: ../../../../v0/util + - name: isDefinedAndNotNull + path: ../../../../v0/util - path: ../../bindings/jsontemplate + - path: '@rudderstack/integrations-lib' steps: - name: validateInput @@ -24,6 +27,7 @@ steps: let eventTypeMap = $.eventTypeMapping(.destination.Config); let event = .message.event.trim().toLowerCase(); let eventType = .message.properties.eventType ?? eventTypeMap[event]; + let eventSubType = .message.properties.eventSubtype && eventType === 'conversion' && (.message.properties.eventSubtype in $.ALLOWED_EVENT_SUBTYPES) ? .message.properties.eventSubtype; $.assert(eventType, "eventType is mandatory for track call"); let payload = .message.().({ index: .properties.index, @@ -32,12 +36,28 @@ steps: filters: .properties.filters, objectIDs: .properties.objectIds, positions: .properties.positions, + value: $.isDefinedAndNotNull(.properties.currency) ? .properties.value, + currency: .properties.currency, userToken: {{{{$.getGenericPaths("userId", "||")}}}}, eventName: event, - eventType: eventType + eventType: eventType, + eventSubtype: eventSubType }); $.context.payload = $.genericpayloadValidator(payload); + - name: prepareObjectDataBlock + condition: $.context.payload.eventType === "conversion" && $.isDefinedAndNotNull(^.message.properties.products) && Array.isArray(^.message.properties.products) + description: | + Populate list of objectData + template: | + const products = ^.message.properties.products + products.($.removeUndefinedAndNullValues({ + "queryID" : $.isDefinedAndNotNull(.queryID) ? String(.queryID) : null, + "price": $.isDefinedAndNotNull(.price) && $.isDefinedAndNotNull(^.message.properties.currency) ? String(.price) : null, + "quantity": $.isDefinedAndNotNull(.quantity)? Number(.quantity) : null, + "discount": $.isDefinedAndNotNull(.discount) ? String(.discount) : null + }))[] + - name: populateProductsData condition: | .message.properties.products && @@ -55,11 +75,13 @@ steps: const products = .message.properties.products; const objectIDs = ~r products.objectId; $.context.payload.objectIDs = Array.isArray(objectIDs) ? objectIDs[:20]:$.context.payload.objectIDs; + $.context.payload.objectData = $.outputs.prepareObjectDataBlock - name: validateDestPayload template: | const filters = $.context.payload.filters; const objectIDs = $.context.payload.objectIDs; + const objectData = $.context.payload.objectData; $.assert(!(filters && objectIDs), "event can't have both objectIds and filters at the same time."); $.assert(filters.length || objectIDs.length, "Either filters or objectIds is required and must be non empty."); diff --git a/src/v0/destinations/algolia/config.js b/src/v0/destinations/algolia/config.js index 11b4ec99f2..4e20294dd2 100644 --- a/src/v0/destinations/algolia/config.js +++ b/src/v0/destinations/algolia/config.js @@ -5,6 +5,7 @@ const CONFIG_CATEGORIES = { TRACK: { type: 'track', name: 'AlgoliaTrack' }, }; const EVENT_TYPES = ['click', 'view', 'conversion']; +const ALLOWED_EVENT_SUBTYPES = ['addToCart', 'purchase']; const MAX_BATCH_SIZE = 1000; const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); module.exports = { @@ -12,4 +13,5 @@ module.exports = { MAX_BATCH_SIZE, EVENT_TYPES, trackMapping: MAPPING_CONFIG[CONFIG_CATEGORIES.TRACK.name], + ALLOWED_EVENT_SUBTYPES, }; diff --git a/src/v0/destinations/algolia/data/AlgoliaTrack.json b/src/v0/destinations/algolia/data/AlgoliaTrack.json index bdc3449147..41f43af9cb 100644 --- a/src/v0/destinations/algolia/data/AlgoliaTrack.json +++ b/src/v0/destinations/algolia/data/AlgoliaTrack.json @@ -34,5 +34,15 @@ "destKey": "positions", "sourceKeys": "properties.positions", "required": false + }, + { + "destKey": "value", + "sourceKeys": "properties.value", + "required": false + }, + { + "destKey": "currency", + "sourceKeys": "properties.currency", + "required": false } ] diff --git a/src/v0/destinations/algolia/transform.js b/src/v0/destinations/algolia/transform.js index 8e9cd57e8b..33ae6f2101 100644 --- a/src/v0/destinations/algolia/transform.js +++ b/src/v0/destinations/algolia/transform.js @@ -16,7 +16,7 @@ const { handleRtTfSingleEventError, } = require('../../util/index'); -const { ENDPOINT, MAX_BATCH_SIZE, trackMapping } = require('./config'); +const { ENDPOINT, MAX_BATCH_SIZE, trackMapping, ALLOWED_EVENT_SUBTYPES } = require('./config'); const { genericpayloadValidator, @@ -38,6 +38,12 @@ const trackResponseBuilder = (message, { Config }) => { const eventMapping = eventTypeMapping(Config); payload.eventName = event; payload.eventType = getValueFromMessage(message, 'properties.eventType') || eventMapping[event]; + if ( + payload.eventType === 'conversion' && + ALLOWED_EVENT_SUBTYPES.includes(getValueFromMessage(message, 'properties.eventSubtype')) + ) { + payload.eventSubtype = getValueFromMessage(message, 'properties.eventSubtype'); + } if (!payload.eventType) { throw new InstrumentationError('eventType is mandatory for track call'); @@ -47,9 +53,13 @@ const trackResponseBuilder = (message, { Config }) => { if (event === 'product list viewed' || event === 'order completed') { const products = getValueFromMessage(message, 'properties.products'); if (products) { - const { objectList, positionList } = createObjectArray(products, payload.eventType); + const { objectList, positionList, objectData } = createObjectArray( + products, + payload.eventType, + ); const objLen = objectList.length; const posLen = positionList.length; + const objDataLen = objectData.length; if (objLen > 0) { payload.objectIDs = objectList; payload.objectIDs.splice(20); @@ -58,10 +68,20 @@ const trackResponseBuilder = (message, { Config }) => { payload.positions = positionList; payload.positions.splice(20); } + + if (objDataLen > 0) { + payload.objectData = objectData; + } // making size of object list and position list equal if (posLen > 0 && objLen > 0 && posLen !== objLen) { throw new InstrumentationError('length of objectId and position should be equal'); } + + if (objDataLen > 0 && objLen > 0 && objDataLen !== objLen) { + throw new InstrumentationError( + 'length of objectId and properties.products array should be equal', + ); + } } } // for all events either filter or objectID should be there diff --git a/src/v0/destinations/algolia/util.js b/src/v0/destinations/algolia/util.js index eddb4dc16d..863c23aba7 100644 --- a/src/v0/destinations/algolia/util.js +++ b/src/v0/destinations/algolia/util.js @@ -1,4 +1,8 @@ -const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + InstrumentationError, + isDefined, + removeUndefinedAndNullValues, +} = require('@rudderstack/integrations-lib'); const logger = require('../../../logger'); const { EVENT_TYPES } = require('./config'); @@ -66,6 +70,8 @@ const genericpayloadValidator = (payload) => { const createObjectArray = (objects, eventType) => { const objectList = []; const positionList = []; + // eslint-disable-next-line sonarjs/no-unused-collection + const objectData = []; if (objects.length > 0) { objects.forEach((object, index) => { if (object.objectId) { @@ -80,13 +86,22 @@ const createObjectArray = (objects, eventType) => { } } else { objectList.push(object.objectId); + if (eventType === 'conversion') { + const singleObjData = { + queryID: isDefined(object.queryID) ? `${object.queryID}` : null, + price: isDefined(object.price) ? `${object.price}` : null, + quantity: object.quantity, + discount: isDefined(object.discount) ? `${object.discount}` : null, + }; + objectData.push(removeUndefinedAndNullValues(singleObjData)); + } } } else { logger.error(`object at index ${index} dropped. objectId is required.`); } }); } - return { objectList, positionList }; + return { objectList, positionList, objectData }; }; const clickPayloadValidator = (payload) => { diff --git a/test/integrations/destinations/algolia/processor/data.ts b/test/integrations/destinations/algolia/processor/data.ts index 7c37c9642a..a8dd31b51a 100644 --- a/test/integrations/destinations/algolia/processor/data.ts +++ b/test/integrations/destinations/algolia/processor/data.ts @@ -1627,4 +1627,664 @@ export const data = [ }, }, }, + { + name: 'algolia', + description: + 'For conversion event including product array and subtype addToCart, object data is sent', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + ip: '0.0.0.0', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + page: { + path: '/destinations/ometria', + referrer: '', + search: '', + title: '', + url: 'https://docs.rudderstack.com/destinations/ometria', + category: 'destination', + initial_referrer: 'https://docs.rudderstack.com', + initial_referring_domain: 'docs.rudderstack.com', + }, + }, + type: 'track', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + properties: { + index: 'products', + eventSubtype: 'addToCart', + products: [ + { + objectId: 'ecommerce-sample-data-919', + position: 7, + quantity: '2', + price: 10, + queryID: '123', + discount: '10', + }, + { + objectId: '9780439784542', + position: 8, + quantity: '3', + price: 30, + queryID: '123', + discount: '10', + }, + ], + queryId: '43b15df305339e827f0ac0bdc5ebcaa7', + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + apiKey: 'dummyApiKey', + applicationId: 'O2YARRI15I', + eventTypeSettings: [ + { + from: 'product list viewed', + to: 'conversion', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + JSON: { + events: [ + { + index: 'products', + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + objectIDs: ['ecommerce-sample-data-919', '9780439784542'], + userToken: 'testuserId1', + eventName: 'product list viewed', + eventSubtype: 'addToCart', + eventType: 'conversion', + objectData: [ + { + quantity: 2, + queryID: '123', + discount: '10', + }, + { + quantity: 3, + queryID: '123', + discount: '10', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insights.algolia.io/1/events', + headers: { + 'X-Algolia-Application-Id': 'O2YARRI15I', + 'X-Algolia-API-Key': 'dummyApiKey', + }, + params: {}, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, + { + name: 'algolia', + description: + 'For conversion event including product array and subtype purchase, object data is sent', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + ip: '0.0.0.0', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + page: { + path: '/destinations/ometria', + referrer: '', + search: '', + title: '', + url: 'https://docs.rudderstack.com/destinations/ometria', + category: 'destination', + initial_referrer: 'https://docs.rudderstack.com', + initial_referring_domain: 'docs.rudderstack.com', + }, + }, + type: 'track', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + properties: { + index: 'products', + eventSubtype: 'purchase', + products: [ + { + objectId: 'ecommerce-sample-data-919', + position: 7, + quantity: '2', + price: 10, + queryID: '123', + discount: '10', + }, + { + objectId: '9780439784542', + position: 8, + quantity: '3', + price: 30, + queryID: '123', + discount: '10', + }, + ], + queryId: '43b15df305339e827f0ac0bdc5ebcaa7', + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + apiKey: 'dummyApiKey', + applicationId: 'O2YARRI15I', + eventTypeSettings: [ + { + from: 'product list viewed', + to: 'conversion', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + JSON: { + events: [ + { + index: 'products', + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + objectIDs: ['ecommerce-sample-data-919', '9780439784542'], + userToken: 'testuserId1', + eventName: 'product list viewed', + eventSubtype: 'purchase', + eventType: 'conversion', + objectData: [ + { + quantity: 2, + queryID: '123', + discount: '10', + }, + { + quantity: 3, + queryID: '123', + discount: '10', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insights.algolia.io/1/events', + headers: { + 'X-Algolia-Application-Id': 'O2YARRI15I', + 'X-Algolia-API-Key': 'dummyApiKey', + }, + params: {}, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, + { + name: 'algolia', + description: + 'For conversion event including product array and subtype wrong, object data is sent but subType is omitted', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + ip: '0.0.0.0', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + page: { + path: '/destinations/ometria', + referrer: '', + search: '', + title: '', + url: 'https://docs.rudderstack.com/destinations/ometria', + category: 'destination', + initial_referrer: 'https://docs.rudderstack.com', + initial_referring_domain: 'docs.rudderstack.com', + }, + }, + type: 'track', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + properties: { + index: 'products', + eventSubtype: 'random', + value: 10, + currency: 'USD', + products: [ + { + objectId: 'ecommerce-sample-data-919', + position: 7, + quantity: '2', + queryID: '123', + discount: '10', + price: 10, + }, + { + objectId: '9780439784542', + position: 8, + quantity: '3', + queryID: '123', + discount: '10', + price: 10, + }, + ], + queryId: '43b15df305339e827f0ac0bdc5ebcaa7', + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + apiKey: 'dummyApiKey', + applicationId: 'O2YARRI15I', + eventTypeSettings: [ + { + from: 'product list viewed', + to: 'conversion', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + JSON: { + events: [ + { + index: 'products', + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + objectIDs: ['ecommerce-sample-data-919', '9780439784542'], + userToken: 'testuserId1', + eventName: 'product list viewed', + eventType: 'conversion', + value: 10, + currency: 'USD', + objectData: [ + { + price: '10', + quantity: 2, + queryID: '123', + discount: '10', + }, + { + price: '10', + quantity: 3, + queryID: '123', + discount: '10', + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insights.algolia.io/1/events', + headers: { + 'X-Algolia-Application-Id': 'O2YARRI15I', + 'X-Algolia-API-Key': 'dummyApiKey', + }, + params: {}, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, + { + name: 'algolia', + description: + 'For conversion event without including product array and subtype purchase, object data is not sent but subType is sent', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + traits: { + email: 'testone@gmail.com', + firstName: 'test', + lastName: 'one', + }, + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36', + locale: 'en-US', + ip: '0.0.0.0', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + page: { + path: '/destinations/ometria', + referrer: '', + search: '', + title: '', + url: 'https://docs.rudderstack.com/destinations/ometria', + category: 'destination', + initial_referrer: 'https://docs.rudderstack.com', + initial_referring_domain: 'docs.rudderstack.com', + }, + }, + type: 'track', + messageId: '84e26acc-56a5-4835-8233-591137fca468', + session_id: '3049dc4c-5a95-4ccd-a3e7-d74a7e411f22', + originalTimestamp: '2019-10-14T09:03:17.562Z', + anonymousId: '123456', + event: 'product list viewed', + userId: 'testuserId1', + properties: { + index: 'products', + eventSubtype: 'purchase', + filters: ['field1:hello', 'val1:val2'], + queryId: '43b15df305339e827f0ac0bdc5ebcaa7', + }, + integrations: { + All: true, + }, + sentAt: '2019-10-14T09:03:22.563Z', + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + destination: { + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + apiKey: 'dummyApiKey', + applicationId: 'O2YARRI15I', + eventTypeSettings: [ + { + from: 'product list viewed', + to: 'conversion', + }, + ], + }, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + JSON: { + events: [ + { + index: 'products', + queryID: '43b15df305339e827f0ac0bdc5ebcaa7', + filters: ['field1:hello', 'val1:val2'], + userToken: 'testuserId1', + eventName: 'product list viewed', + eventType: 'conversion', + eventSubtype: 'purchase', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://insights.algolia.io/1/events', + headers: { + 'X-Algolia-Application-Id': 'O2YARRI15I', + 'X-Algolia-API-Key': 'dummyApiKey', + }, + params: {}, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, ];