Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added more optional parameters to Snap Conversions API destination #1546

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@ import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Definition from '../index'
import { Settings } from '../generated-types'
import { defaultValues } from '@segment/actions-core'

import reportConversionEvent from '../reportConversionEvent'

const DEFAULT_VALS = {
...defaultValues(reportConversionEvent.fields)
}

const testDestination = createTestIntegration(Definition)
const timestamp = '2022-05-12T15:21:15.449Z'
Expand Down Expand Up @@ -193,6 +200,177 @@ describe('Snap Conversions API ', () => {
).rejects.toThrowError('Galleon is not a valid currency code.')
})

it('should fail invalid country', async () => {
nock(conversionEventUrl).post('').reply(400, {})

const event = createTestEvent({
...testEvent,
properties: {
country: 'United States of America'
}
})

await expect(
testDestination.testAction('reportConversionEvent', {
event,
settings,
useDefaultMappings: false,
auth: {
accessToken,
refreshToken
},
mapping: {
...DEFAULT_VALS,
event_type: 'PURCHASE',
event_conversion_type: 'WEB',
country: { '@path': '$.properties.country' }
}
})
).rejects.toThrowError(
'United States of America is not a valid country code. It must be provided as a two letter ISO 3166 alpha-2 country code.'
)
})

it('should fail invalid region', async () => {
nock(conversionEventUrl).post('').reply(400, {})

const event = createTestEvent({
...testEvent,
properties: {
country: 'US',
region: 'California'
}
})

await expect(
testDestination.testAction('reportConversionEvent', {
event,
settings,
useDefaultMappings: false,
auth: {
accessToken,
refreshToken
},
mapping: {
...DEFAULT_VALS,
event_type: 'PURCHASE',
event_conversion_type: 'WEB',
country: { '@path': '$.properties.country' },
region: { '@path': '$.properties.region' }
}
})
).rejects.toThrowError(
'California is not a valid region code. Given that country is US, region should be a two letter State code.'
)
})

it('should handle event with valid country (and no region)', async () => {
nock(conversionEventUrl).post('').reply(200, {})

const event = createTestEvent({
...testEvent,
properties: {
country: 'US'
}
})

const responses = await testDestination.testAction('reportConversionEvent', {
event,
settings,
useDefaultMappings: false,
auth: {
accessToken,
refreshToken
},
mapping: {
...DEFAULT_VALS,
event_type: 'PURCHASE',
event_conversion_type: 'WEB',
country: { '@path': '$.properties.country' },
region: { '@path': '$.properties.region' }
}
})

expect(responses).not.toBeNull()
expect(responses[0].status).toBe(200)

expect(responses[0].options.body).toMatchInlineSnapshot(
`"{\\"integration\\":\\"segment\\",\\"event_type\\":\\"PURCHASE\\",\\"event_conversion_type\\":\\"WEB\\",\\"timestamp\\":1652368875449,\\"user_agent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"hashed_ip_address\\":\\"838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5\\",\\"page_url\\":\\"https://segment.com/academy/\\",\\"country\\":\\"US\\",\\"pixel_id\\":\\"test123\\"}"`
)
})

it('should handle event with valid region (US)', async () => {
nock(conversionEventUrl).post('').reply(200, {})

const event = createTestEvent({
...testEvent,
properties: {
country: 'US',
region: 'CA'
}
})

const responses = await testDestination.testAction('reportConversionEvent', {
event,
settings,
useDefaultMappings: false,
auth: {
accessToken,
refreshToken
},
mapping: {
...DEFAULT_VALS,
event_type: 'PURCHASE',
event_conversion_type: 'WEB',
country: { '@path': '$.properties.country' },
region: { '@path': '$.properties.region' }
}
})

expect(responses).not.toBeNull()
expect(responses[0].status).toBe(200)

expect(responses[0].options.body).toMatchInlineSnapshot(
`"{\\"integration\\":\\"segment\\",\\"event_type\\":\\"PURCHASE\\",\\"event_conversion_type\\":\\"WEB\\",\\"timestamp\\":1652368875449,\\"user_agent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"hashed_ip_address\\":\\"838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5\\",\\"page_url\\":\\"https://segment.com/academy/\\",\\"country\\":\\"US\\",\\"region\\":\\"CA\\",\\"pixel_id\\":\\"test123\\"}"`
)
})

it('should handle event with valid region (non-US)', async () => {
nock(conversionEventUrl).post('').reply(200, {})

const event = createTestEvent({
...testEvent,
properties: {
country: 'FR',
region: 'Hauts-de-France'
}
})

const responses = await testDestination.testAction('reportConversionEvent', {
event,
settings,
useDefaultMappings: false,
auth: {
accessToken,
refreshToken
},
mapping: {
...DEFAULT_VALS,
event_type: 'PURCHASE',
event_conversion_type: 'WEB',
country: { '@path': '$.properties.country' },
region: { '@path': '$.properties.region' }
}
})

expect(responses).not.toBeNull()
expect(responses[0].status).toBe(200)

expect(responses[0].options.body).toMatchInlineSnapshot(
`"{\\"integration\\":\\"segment\\",\\"event_type\\":\\"PURCHASE\\",\\"event_conversion_type\\":\\"WEB\\",\\"timestamp\\":1652368875449,\\"user_agent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"hashed_ip_address\\":\\"838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5\\",\\"page_url\\":\\"https://segment.com/academy/\\",\\"country\\":\\"FR\\",\\"region\\":\\"Hauts-de-France\\",\\"pixel_id\\":\\"test123\\"}"`
)
})

it('should fail missing event conversion type', async () => {
nock(conversionEventUrl).post('').reply(400, {})

Expand Down Expand Up @@ -399,5 +577,60 @@ describe('Snap Conversions API ', () => {
`"{\\"integration\\":\\"segment\\",\\"event_type\\":\\"PURCHASE\\",\\"event_conversion_type\\":\\"WEB\\",\\"timestamp\\":1652368875449,\\"user_agent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"hashed_ip_address\\":\\"838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5\\",\\"page_url\\":\\"https://segment.com/academy/\\",\\"pixel_id\\":\\"test123\\"}"`
)
})

it('should handle event with optional parameters', async () => {
nock(conversionEventUrl).post('').reply(200, {})

const event = createTestEvent({
...testEvent,
properties: {
...testEvent.properties,
idfv: 'C305F2DB-56FC-404F-B6C1-BC52E0B680D8',
first_name: 'John',
middle_name: 'Middle',
last_name: 'Doe',
city: 'Santa Monica',
state: 'CA',
zip: '90405',
dob_month: 'January',
dob_day: '26',
country: 'US',
region: 'CA'
}
})

const responses = await testDestination.testAction('reportConversionEvent', {
event,
settings,
useDefaultMappings: false,
auth: {
accessToken,
refreshToken
},
mapping: {
...DEFAULT_VALS,
event_type: 'PURCHASE',
event_conversion_type: 'WEB',
idfv: { '@path': '$.properties.idfv' },
first_name: { '@path': '$.properties.first_name' },
middle_name: { '@path': '$.properties.middle_name' },
last_name: { '@path': '$.properties.last_name' },
city: { '@path': '$.properties.city' },
state: { '@path': '$.properties.state' },
zip: { '@path': '$.properties.zip' },
dob_month: { '@path': '$.properties.dob_month' },
dob_day: { '@path': '$.properties.dob_day' },
country: { '@path': '$.properties.country' },
region: { '@path': '$.properties.region' }
}
})

expect(responses).not.toBeNull()
expect(responses[0].status).toBe(200)

expect(responses[0].options.body).toMatchInlineSnapshot(
`"{\\"integration\\":\\"segment\\",\\"event_type\\":\\"PURCHASE\\",\\"event_conversion_type\\":\\"WEB\\",\\"timestamp\\":1652368875449,\\"hashed_email\\":\\"cc779c04191c2e736d89e45c11339c8382832bcaf70383f7df94e3d08ba7a6d9\\",\\"hashed_idfv\\":\\"f18c8b858e52b0c49f7db8f813538db0ecdc513357efd62c525784a9beb617d6\\",\\"hashed_phone_number\\":\\"dc008fda46e2e64002cf2f82a4906236282d431c4f75e5b60bfe79fc48546383\\",\\"user_agent\\":\\"Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1\\",\\"hashed_ip_address\\":\\"838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5\\",\\"price\\":15,\\"currency\\":\\"USD\\",\\"page_url\\":\\"https://segment.com/academy/\\",\\"hashed_first_name_sha\\":\\"a8cfcd74832004951b4408cdb0a5dbcd8c7e52d43f7fe244bf720582e05241da\\",\\"hashed_middle_name_sha\\":\\"d93006ec2e4339d770a7afd068c1f1e789a52df12f595e529fd0f302fc1e5ec7\\",\\"hashed_last_name_sha\\":\\"fd53ef835b15485572a6e82cf470dcb41fd218ae5751ab7531c956a2a6bcd3c7\\",\\"hashed_city_sha\\":\\"ced2e77f732fbf327f53e1f9748078c778b190ed9cf376a7df469a92d2ad62d3\\",\\"hashed_state_sha\\":\\"4b650e5c4785025dee7bd65e3c5c527356717d7a1c0bfef5b4ada8ca1e9cbe17\\",\\"hashed_zip\\":\\"e222c384dd83ac669bcd1da281ffea2e60bab298f8c0673d35bc0b704e345282\\",\\"hashed_dob_month\\":\\"37082e68df858e0ba76442174128811135890ae4c2c5df8b6f31aef5885d0be7\\",\\"hashed_dob_day\\":\\"5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca\\",\\"country\\":\\"US\\",\\"region\\":\\"CA\\",\\"pixel_id\\":\\"test123\\"}"`
)
})
})
})

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,22 @@ import {
page_url,
sign_up_method,
formatPayload,
COUNTRY_ISO_3166_CODES,
CURRENCY_ISO_4217_CODES,
conversionType,
device_model,
os_version,
click_id
click_id,
first_name,
middle_name,
last_name,
city,
state,
zip,
dob_month,
dob_day,
country,
region
} from '../snap-capi-properties'

const CONVERSION_EVENT_URL = 'https://tr.snapchat.com/v2/conversion'
Expand Down Expand Up @@ -65,7 +76,17 @@ const action: ActionDefinition<Settings, Payload> = {
sign_up_method: sign_up_method,
os_version: os_version,
device_model: device_model,
click_id: click_id
click_id: click_id,
first_name: first_name,
middle_name: middle_name,
last_name: last_name,
city: city,
state: state,
zip: zip,
dob_month: dob_month,
dob_day: dob_day,
country: country,
region: region
},
perform: (request, data) => {
if (data.payload.currency && !CURRENCY_ISO_4217_CODES.has(data.payload.currency.toUpperCase())) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if for this message, we should send Misconfigured optional field rather than Misconfigured required field, as I think currency isn't mandatory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hi @sebashine-branly , yes I agree. The error can better be explained as "Misconfigured optional field".

However, could we throw a PayloadValidationError instead please?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to make the change but I have some concerns about the comment above the definition of PayloadValidationError: https://github.com/segmentio/action-destinations/blob/main/packages/core/src/errors.ts#L76

Error to indicate the payload is missing fields that are required.

The field here is not required so I am not sure whether the new error type is fitting. I don't know if the comment should be updated instead, to also include the case of wrongly formatted optional parameters, but if needed I would prefer to have it in a separate PR as the contribution would be outside of the snap-conversions-api folder.

https://github.com/segmentio/action-destinations/blob/main/CONTRIBUTING.md#submitting-changes-after-your-integration-is-already-live

Do not raise a PR containing changes for more than 1 Integration. For example if you need to make changes to a Cloud Mode and Device Mode Integration you should raise separate PRs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also noticed that PayloadValidationError uses ErrorCodes.PAYLOAD_VALIDATION_FAILED behind the scenes but it seems like ErrorCodes.INVALID_CURRENCY_CODE would be more fitting.

Expand All @@ -76,6 +97,22 @@ const action: ActionDefinition<Settings, Payload> = {
)
}

if (data.payload.country && !COUNTRY_ISO_3166_CODES.has(data.payload.country)) {
throw new IntegrationError(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @sebashine-branly can we throw a PayloadValidationError error instead please?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern regarding the fact this field is not required, contrary to what is assumed by the comment above PayloadValidationError definition.

`${data.payload.country} is not a valid country code. It must be provided as a two letter ISO 3166 alpha-2 country code.`,
'Misconfigured optional field',
400
)
}

if (data.payload.country === 'US' && data.payload.region && data.payload.region.length !== 2) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be a PayloadValidationError please?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same concern regarding the fact this field is not required, contrary to what is assumed by the comment above PayloadValidationError definition.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, I noticed that this other case could use PayloadValidationError without any confusion as it is really about required fields, if you agree?
https://github.com/segmentio/action-destinations/blob/main/packages/destination-actions/src/destinations/snap-conversions-api/reportConversionEvent/index.ts#L85

throw new IntegrationError(
`${data.payload.region} is not a valid region code. Given that country is US, region should be a two letter State code.`,
'Misconfigured optional field',
400
)
}

if (
!data.payload.email &&
!data.payload.phone_number &&
Expand Down