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/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/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/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/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,