diff --git a/.eslintignore b/.eslintignore index ce54730f4b..b1bbed4416 100644 --- a/.eslintignore +++ b/.eslintignore @@ -21,3 +21,8 @@ src/v0/destinations/personalize/scripts/ test/integrations/destinations/testTypes.d.ts *.config*.js scripts/skipPrepareScript.js +*.yaml +*.yml +.eslintignore +.prettierignore +*.json \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index 556470697d..144b90e348 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -19,7 +19,8 @@ "parserOptions": { "ecmaVersion": 12, "sourceType": "module", - "project": "./tsconfig.json" + "project": "./tsconfig.json", + "extraFileExtensions": [".yaml"] }, "rules": { "unicorn/filename-case": [ diff --git a/.prettierignore b/.prettierignore index 99747b29bb..ac5f1fc409 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,5 @@ test/**/*.js src/util/lodash-es-core.js src/util/url-search-params.min.js dist +.eslintignore +.prettierignore diff --git a/src/cdk/v2/destinations/smartly/config.js b/src/cdk/v2/destinations/smartly/config.js new file mode 100644 index 0000000000..5083fde5fe --- /dev/null +++ b/src/cdk/v2/destinations/smartly/config.js @@ -0,0 +1,21 @@ +const { getMappingConfig } = require('../../../../v0/util'); + +const ConfigCategories = { + TRACK: { + type: 'track', + name: 'trackMapping', + }, +}; + +const mappingConfig = getMappingConfig(ConfigCategories, __dirname); +const singleEventEndpoint = 'https://s2s.smartly.io/events'; +const batchEndpoint = 'https://s2s.smartly.io/events/batch'; + +module.exports = { + ConfigCategories, + mappingConfig, + singleEventEndpoint, + batchEndpoint, + TRACK_CONFIG: mappingConfig[ConfigCategories.TRACK.name], + MAX_BATCH_SIZE: 1000, +}; diff --git a/src/cdk/v2/destinations/smartly/data/trackMapping.json b/src/cdk/v2/destinations/smartly/data/trackMapping.json new file mode 100644 index 0000000000..55ba437f12 --- /dev/null +++ b/src/cdk/v2/destinations/smartly/data/trackMapping.json @@ -0,0 +1,76 @@ +[ + { + "destKey": "value", + "sourceKeys": [ + "properties.total", + "properties.value", + "properties.revenue", + { + "operation": "multiplication", + "args": [ + { + "sourceKeys": "properties.price" + }, + { + "sourceKeys": "properties.quantity", + "default": 1 + } + ] + } + ], + "metadata": { + "type": "toNumber" + }, + "required": false + }, + { + "sourceKeys": ["properties.conversions", "properties.products.length"], + "required": false, + "metadata": { + "defaultValue": "1" + }, + "destKey": "conversions" + }, + { + "sourceKeys": ["properties.adUnitId", "properties.ad_unit_id"], + "required": true, + "destKey": "ad_unit_id", + "metadata": { + "type": "toString" + } + }, + { + "sourceKeys": ["properties.platform"], + "required": true, + "destKey": "platform" + }, + { + "sourceKeys": ["properties.adInteractionTime", "properties.ad_interaction_time"], + "required": true, + "metadata": { + "type": "secondTimestamp" + }, + "destKey": "ad_interaction_time" + }, + { + "sourceKeys": ["properties.installTime"], + "required": false, + "metadata": { + "type": "secondTimestamp" + }, + "destKey": "installTime" + }, + { + "sourceKeys": ["originalTimestamp", "timestamp"], + "required": false, + "metadata": { + "type": "secondTimestamp" + }, + "destKey": "event_time" + }, + { + "sourceKeys": ["properties.currency"], + "required": false, + "destKey": "value_currency" + } +] diff --git a/src/cdk/v2/destinations/smartly/procWorkflow.yaml b/src/cdk/v2/destinations/smartly/procWorkflow.yaml new file mode 100644 index 0000000000..b69df0dd09 --- /dev/null +++ b/src/cdk/v2/destinations/smartly/procWorkflow.yaml @@ -0,0 +1,31 @@ +bindings: + - name: EventType + path: ../../../../constants + - path: ../../bindings/jsontemplate + - name: defaultRequestConfig + path: ../../../../v0/util + - name: removeUndefinedAndNullValues + path: ../../../../v0/util + - name: constructPayload + path: ../../../../v0/util + - path: ./config + - 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])}}, "message type " + messageType + " is not supported"); + $.assertConfig(.destination.Config.apiToken, "API Token is not present. Aborting"); + - name: preparePayload + template: | + const payload = $.removeUndefinedAndNullValues($.constructPayload(.message, $.TRACK_CONFIG)); + $.verifyAdInteractionTime(payload.ad_interaction_time); + $.context.payloadList = $.getPayloads(.message.event, .destination.Config, payload) + - name: buildResponse + template: | + const response = $.buildResponseList($.context.payloadList) + response diff --git a/src/cdk/v2/destinations/smartly/rtWorkflow.yaml b/src/cdk/v2/destinations/smartly/rtWorkflow.yaml new file mode 100644 index 0000000000..4d3afdb6d0 --- /dev/null +++ b/src/cdk/v2/destinations/smartly/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, + "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/smartly/utils.js b/src/cdk/v2/destinations/smartly/utils.js new file mode 100644 index 0000000000..7d53ed0d27 --- /dev/null +++ b/src/cdk/v2/destinations/smartly/utils.js @@ -0,0 +1,108 @@ +const { BatchUtils } = require('@rudderstack/workflow-engine'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const moment = require('moment'); +const config = require('./config'); +const { + getHashFromArrayWithDuplicate, + defaultRequestConfig, + isDefinedAndNotNull, +} = require('../../../../v0/util'); + +// docs reference : https://support.smartly.io/hc/en-us/articles/4406049685788-S2S-integration-API-description#01H8HBXZF6WSKSYBW1C6NY8A88 + +/** + * This function generates an array of payload objects, each with the event property set + * to different values associated with the given event name according to eventsMapping + * @param {*} event + * @param {*} eventsMapping + * @param {*} payload + * @returns + */ +const getPayloads = (event, Config, payload) => { + if (!isDefinedAndNotNull(event) || typeof event !== 'string') { + throw new InstrumentationError('Event is not defined or is not String'); + } + const eventsMap = getHashFromArrayWithDuplicate(Config.eventsMapping); + // eventsMap = hashmap {"prop1":["val1","val2"],"prop2":["val2"]} + const eventList = Array.isArray(eventsMap[event.toLowerCase()]) + ? eventsMap[event.toLowerCase()] + : Array.from(eventsMap[event.toLowerCase()] || [event]); + + const payloadLists = eventList.map((ev) => ({ ...payload, event_name: ev })); + return payloadLists; +}; + +// ad_interaction_time must be within one year in the future and three years in the past from the current date +// Example : "1735680000" +const verifyAdInteractionTime = (adInteractionTime) => { + if (isDefinedAndNotNull(adInteractionTime)) { + const now = moment(); + const threeYearAgo = now.clone().subtract(3, 'year'); + const oneYearFromNow = now.clone().add(1, 'year'); + const inputMoment = moment(adInteractionTime * 1000); // Convert to milliseconds + if (!inputMoment.isAfter(threeYearAgo) || !inputMoment.isBefore(oneYearFromNow)) { + throw new InstrumentationError( + 'ad_interaction_time must be within one year in the future and three years in the past.', + ); + } + } +}; + +const buildResponseList = (payloadList) => + payloadList.map((payload) => { + const response = defaultRequestConfig(); + response.body.JSON = payload; + response.endpoint = config.singleEventEndpoint; + response.method = 'POST'; + return response; + }); + +const batchBuilder = (batch, destination) => ({ + batchedRequest: { + body: { + JSON: { events: batch.map((event) => event.output) }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: config.batchEndpoint, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${destination.Config.apiToken}`, + }, + params: {}, + files: {}, + }, + metadata: batch + .map((event) => event.metadata) + .filter((metadata, index, self) => self.findIndex((m) => m.jobId === metadata.jobId) === index), // handling jobId duplication for multiplexed events + 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) => { + if (events.length === 0) { + return []; + } + const { destination } = events[0]; + const batches = BatchUtils.chunkArrayBySizeAndLength(events, { maxItems: config.MAX_BATCH_SIZE }); + + const response = []; + batches.items.forEach((batch) => { + const batchedResponse = batchBuilder(batch, destination); + response.push(batchedResponse); + }); + return response; +}; + +module.exports = { batchResponseBuilder, getPayloads, buildResponseList, verifyAdInteractionTime }; diff --git a/src/cdk/v2/destinations/smartly/utils.test.js b/src/cdk/v2/destinations/smartly/utils.test.js new file mode 100644 index 0000000000..0ad73f5369 --- /dev/null +++ b/src/cdk/v2/destinations/smartly/utils.test.js @@ -0,0 +1,59 @@ +const moment = require('moment'); +const { verifyAdInteractionTime } = require('./utils'); + +describe('verifyAdInteractionTime', () => { + it('should pass when adInteractionTime is 2 years in the past (UNIX timestamp)', () => { + // 2 years ago from now + const adInteractionTime = moment().subtract(2, 'years').unix(); + expect(() => verifyAdInteractionTime(adInteractionTime)).not.toThrow(); + }); + + it('should pass when adInteractionTime is 10 months in the future (UNIX timestamp)', () => { + // 10 months in the future from now + const adInteractionTime = moment().add(10, 'months').unix(); + expect(() => verifyAdInteractionTime(adInteractionTime)).not.toThrow(); + }); + + it('should fail when adInteractionTime is 4 years in the past (UNIX timestamp)', () => { + // 4 years ago from now + const adInteractionTime = moment().subtract(4, 'years').unix(); + expect(() => verifyAdInteractionTime(adInteractionTime)).toThrow( + 'ad_interaction_time must be within one year in the future and three years in the past.', + ); + }); + + it('should fail when adInteractionTime is 2 years in the future (UNIX timestamp)', () => { + // 2 years in the future from now + const adInteractionTime = moment().add(2, 'years').unix(); + expect(() => verifyAdInteractionTime(adInteractionTime)).toThrow( + 'ad_interaction_time must be within one year in the future and three years in the past.', + ); + }); + + it('should pass when adInteractionTime is exactly 1 year in the future (UTC date string)', () => { + // Exactly 1 year in the future from now + const adInteractionTime = moment.utc().add(1, 'year').toISOString(); + expect(() => verifyAdInteractionTime(adInteractionTime)).toThrow(); + }); + + it('should fail when adInteractionTime is 4 years in the past (UTC date string)', () => { + // 4 years ago from now + const adInteractionTime = moment.utc().subtract(4, 'years').toISOString(); + expect(() => verifyAdInteractionTime(adInteractionTime)).toThrow( + 'ad_interaction_time must be within one year in the future and three years in the past.', + ); + }); + + it('should fail when adInteractionTime is 2 years in the future (UTC date string)', () => { + // 2 years in the future from now + const adInteractionTime = moment.utc().add(2, 'years').toISOString(); + expect(() => verifyAdInteractionTime(adInteractionTime)).toThrow( + 'ad_interaction_time must be within one year in the future and three years in the past.', + ); + }); + + it('should not throw an error if adInteractionTime is null or undefined', () => { + expect(() => verifyAdInteractionTime(null)).not.toThrow(); + expect(() => verifyAdInteractionTime(undefined)).not.toThrow(); + }); +}); diff --git a/src/features.json b/src/features.json index 1a69a58307..a0261683a8 100644 --- a/src/features.json +++ b/src/features.json @@ -77,7 +77,8 @@ "CLICKSEND": true, "ZOHO": true, "CORDIAL": true, - "BLOOMREACH_CATALOG": true + "BLOOMREACH_CATALOG": true, + "SMARTLY": true }, "regulations": [ "BRAZE", diff --git a/test/integrations/destinations/smartly/commonConfig.ts b/test/integrations/destinations/smartly/commonConfig.ts new file mode 100644 index 0000000000..f5b0a6f4d4 --- /dev/null +++ b/test/integrations/destinations/smartly/commonConfig.ts @@ -0,0 +1,40 @@ +export const destination = { + ID: 'random_id', + Name: 'smartly', + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + Config: { + apiToken: 'testAuthToken', + eventsMapping: [ + { + from: 'product list viewed', + to: 'event1', + }, + { + from: 'product list viewed', + to: 'event2', + }, + ], + }, +}; + +export const routerInstrumentationErrorStatTags = { + destType: 'SMARTLY', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'router', + implementation: 'cdkV2', + module: 'destination', +}; +export const processInstrumentationErrorStatTags = { + destType: 'SMARTLY', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + feature: 'processor', + implementation: 'cdkV2', + module: 'destination', + destinationId: 'dummyDestId', +}; diff --git a/test/integrations/destinations/smartly/mocks.ts b/test/integrations/destinations/smartly/mocks.ts new file mode 100644 index 0000000000..78773d8853 --- /dev/null +++ b/test/integrations/destinations/smartly/mocks.ts @@ -0,0 +1,6 @@ +import config from '../../../../src/cdk/v2/destinations/smartly/config'; + +export const defaultMockFns = () => { + jest.useFakeTimers().setSystemTime(new Date('2024-02-01')); + jest.replaceProperty(config, 'MAX_BATCH_SIZE', 2); +}; diff --git a/test/integrations/destinations/smartly/processor/data.ts b/test/integrations/destinations/smartly/processor/data.ts new file mode 100644 index 0000000000..a94f6b220f --- /dev/null +++ b/test/integrations/destinations/smartly/processor/data.ts @@ -0,0 +1,9 @@ +import { trackTestData } from './track'; +import { validationFailures } from './validation'; + +export const mockFns = (_) => { + // @ts-ignore + jest.useFakeTimers().setSystemTime(new Date('2024-02-01')); +}; + +export const data = [...trackTestData, ...validationFailures].map((d) => ({ ...d, mockFns })); diff --git a/test/integrations/destinations/smartly/processor/track.ts b/test/integrations/destinations/smartly/processor/track.ts new file mode 100644 index 0000000000..944327fce3 --- /dev/null +++ b/test/integrations/destinations/smartly/processor/track.ts @@ -0,0 +1,71 @@ +import { destination } from '../commonConfig'; + +export const trackTestData = [ + { + name: 'smartly', + description: 'Test 0', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + event: 'Add to cart', + properties: { + platform: 'meta', + ad_unit_id: '228287', + ad_interaction_time: 1735680000, + email: 'eventIdn01@sample.com', + }, + type: 'track', + userId: 'eventIdn01', + }, + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://s2s.smartly.io/events', + headers: {}, + params: {}, + body: { + JSON: { + platform: 'meta', + ad_unit_id: '228287', + ad_interaction_time: 1735680000, + conversions: '1', + event_name: 'Add to cart', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/smartly/processor/validation.ts b/test/integrations/destinations/smartly/processor/validation.ts new file mode 100644 index 0000000000..996afc34b3 --- /dev/null +++ b/test/integrations/destinations/smartly/processor/validation.ts @@ -0,0 +1,174 @@ +import { processInstrumentationErrorStatTags, destination } from '../commonConfig'; + +export const validationFailures = [ + { + id: 'Smartly-validation-test-1', + name: 'smartly', + 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: 'john123', + properties: { + products: [{}], + ad_unit_id: '22123387', + ad_interaction_time: '1690867200', + }, + 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 ["properties.platform"]: Workflow: procWorkflow, Step: preparePayload, ChildStep: undefined, OriginalError: Missing required value from ["properties.platform"]', + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, + { + id: 'Smartly-test-2', + name: 'smartly', + 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', + event_name: 'purchase', + sentAt: '2021-01-25T16:12:02.048Z', + userId: 'john67', + channel: 'mobile', + rudderId: 'b7b24f86-cccx-46d8-b2b4-ccaxxx80239c', + messageId: 'dummy_msg_id', + properties: { + platform: 'snapchat', + ad_unit_id: '2653387', + ad_interaction_time: '1690867200', + }, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + }, + 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, + }, + ], + }, + }, + }, + { + id: 'Smartly-test-3', + name: 'smartly', + description: 'Event name not defined', + scenario: 'Framework', + successCriteria: 'Transformationn Error for Undefined Event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'track', + sentAt: '2021-01-25T16:12:02.048Z', + userId: 'john67', + channel: 'mobile', + rudderId: 'b7b24f86-cccx-46d8-b2b4-ccaxxx80239c', + messageId: 'dummy_msg_id', + properties: { + platform: 'snapchat', + ad_unit_id: '2653387', + ad_interaction_time: 1675094400, + }, + anonymousId: 'anon_123', + integrations: { + All: true, + }, + }, + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + error: + 'Event is not defined or is not String: Workflow: procWorkflow, Step: preparePayload, ChildStep: undefined, OriginalError: Event is not defined or is not String', + metadata: { + destinationId: 'dummyDestId', + jobId: '1', + }, + statTags: processInstrumentationErrorStatTags, + statusCode: 400, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/smartly/router/data.ts b/test/integrations/destinations/smartly/router/data.ts new file mode 100644 index 0000000000..7c2d74e6f0 --- /dev/null +++ b/test/integrations/destinations/smartly/router/data.ts @@ -0,0 +1,329 @@ +import { destination, routerInstrumentationErrorStatTags } from '../commonConfig'; +import { defaultMockFns } from '../mocks'; + +export const data = [ + { + name: 'smartly', + id: 'Test 0 - router', + description: 'Track call with multiplexing and batching', + 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: { + type: 'track', + event: 'product list viewed', + properties: { + platform: 'meta', + conversions: 1, + ad_unit_id: '221187', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 2, userId: 'u2' }, + destination, + }, + { + message: { + type: 'track', + event: 'add to cart', + properties: { + conversions: 3, + platform: 'snapchat', + ad_unit_id: '77187', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 3, userId: 'u3' }, + destination, + }, + ], + destType: 'smartly', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + body: { + JSON: { + events: [ + { + conversions: 1, + ad_unit_id: '221187', + platform: 'meta', + ad_interaction_time: 1690867200, + event_name: 'event1', + }, + { + conversions: 1, + ad_unit_id: '221187', + platform: 'meta', + ad_interaction_time: 1690867200, + event_name: 'event2', + }, + { + conversions: 3, + ad_unit_id: '77187', + platform: 'snapchat', + ad_interaction_time: 1690867200, + event_name: 'add to cart', + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://s2s.smartly.io/events/batch', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer testAuthToken`, + }, + params: {}, + files: {}, + }, + metadata: [ + { + jobId: 2, + userId: 'u2', + }, + { + jobId: 3, + userId: 'u3', + }, + ], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + mockFns: () => { + jest.useFakeTimers().setSystemTime(new Date('2024-02-01')); + }, + }, + { + name: 'smartly', + id: 'Test 1 - router', + description: 'Batch calls with 4 succesfull events including multiplexing and 2 failed events', + 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: { + type: 'track', + event: 'product list viewed', + properties: { + platform: 'meta', + conversions: 1, + ad_unit_id: '221187', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 11, userId: 'u1' }, + destination, + }, + { + message: { + type: 'track', + event: 'purchase', + userId: 'testuserId1', + integrations: { All: true }, + properties: { + conversions: 3, + platform: 'snapchat', + ad_unit_id: '77187', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 13, userId: 'u1' }, + destination, + }, + { + message: { + type: 'track', + userId: 'testuserId1', + integrations: { All: true }, + properties: { + conversions: 3, + platform: 'snapchat', + ad_unit_id: '12387', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 14, userId: 'u1' }, + destination, + }, + { + message: { + type: 'track', + event: 'random event', + userId: 'testuserId1', + integrations: { All: true }, + properties: { + conversions: 3, + ad_unit_id: '77187', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 15, userId: 'u1' }, + destination, + }, + { + message: { + type: 'track', + event: 'add to cart', + userId: 'testuserId1', + integrations: { All: true }, + properties: { + conversions: 3, + platform: 'tiktok', + ad_unit_id: '789187', + ad_interaction_time: 1690867200, + }, + }, + metadata: { jobId: 16, userId: 'u1' }, + destination, + }, + ], + destType: 'smartly', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + destination, + error: 'Event is not defined or is not String', + metadata: [{ jobId: 14, userId: 'u1' }], + statTags: routerInstrumentationErrorStatTags, + statusCode: 400, + }, + { + batched: false, + destination, + error: 'Missing required value from ["properties.platform"]', + metadata: [{ jobId: 15, userId: 'u1' }], + statTags: routerInstrumentationErrorStatTags, + statusCode: 400, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://s2s.smartly.io/events/batch', + params: {}, + body: { + FORM: {}, + JSON: { + events: [ + { + platform: 'meta', + conversions: 1, + event_name: 'event1', + ad_unit_id: '221187', + ad_interaction_time: 1690867200, + }, + { + platform: 'meta', + conversions: 1, + event_name: 'event2', + ad_unit_id: '221187', + ad_interaction_time: 1690867200, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer testAuthToken`, + }, + files: {}, + }, + metadata: [{ jobId: 11, userId: 'u1' }], + batched: true, + statusCode: 200, + destination, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://s2s.smartly.io/events/batch', + params: {}, + body: { + FORM: {}, + JSON: { + events: [ + { + conversions: 3, + event_name: 'purchase', + platform: 'snapchat', + ad_unit_id: '77187', + ad_interaction_time: 1690867200, + }, + { + conversions: 3, + event_name: 'add to cart', + platform: 'tiktok', + ad_unit_id: '789187', + ad_interaction_time: 1690867200, + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + }, + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer testAuthToken`, + }, + files: {}, + }, + metadata: [ + { jobId: 13, userId: 'u1' }, + { jobId: 16, userId: 'u1' }, + ], + batched: true, + statusCode: 200, + destination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +];