From 5d80f5ea09eb433f9decc62b8241b96f328c3a66 Mon Sep 17 00:00:00 2001 From: zhadier39 <113626827+zhadier39@users.noreply.github.com> Date: Wed, 29 Nov 2023 16:41:16 +0500 Subject: [PATCH 01/34] [Hyperengage] Resolved issue with Timezone Offset and Added Missing user_id field in group call (#1733) * Add unit and integration tests * Updated field descriptions for group, identify and track * Updated common fields * Fix identify function error * Added authentication endpoint * Revise tests to enable auth * Update default paths for groupId. * Implement recommended changes from PR #1621 * Add url field * Resolve pathing issues and add tests/checks for first and last name fields * Resolve test issues, payload validation error, and correct context default * Fix no user_id field in group call and timezone offset bug * Add tests for new functionality * Delete packages/destination-actions/src/destinations/liveramp-audiences/audienceEnteredSFTP.types.ts Remove auto generated types file on liveramp platform * Fix ts error with timeZoneName --------- Co-authored-by: saadhypng Co-authored-by: Saad Ali <88059697+saadhypng@users.noreply.github.com> --- .../__tests__/validateInput.test.ts | 3 +++ .../hyperengage/group/generated-types.ts | 4 ++++ .../destinations/hyperengage/group/index.ts | 22 +++++++++++++++++-- .../hyperengage/identify/index.ts | 8 ++++++- .../hyperengage/track/generated-types.ts | 8 +++---- .../destinations/hyperengage/track/index.ts | 18 +++++++-------- .../destinations/hyperengage/validateInput.ts | 7 ++++-- 7 files changed, 52 insertions(+), 18 deletions(-) diff --git a/packages/destination-actions/src/destinations/hyperengage/__tests__/validateInput.test.ts b/packages/destination-actions/src/destinations/hyperengage/__tests__/validateInput.test.ts index 1298d085b1..00417afb92 100644 --- a/packages/destination-actions/src/destinations/hyperengage/__tests__/validateInput.test.ts +++ b/packages/destination-actions/src/destinations/hyperengage/__tests__/validateInput.test.ts @@ -41,6 +41,7 @@ const fakeGroupData = { required: 'false' }, timestamp: '2023-09-11T08:06:11.192Z', + timezone: 'Europe/Amsterdam', user_id: 'test', account_id: 'testAccount' } @@ -75,10 +76,12 @@ describe('validateInput', () => { it('should return converted payload', async () => { const payload = validateInput(settings, fakeGroupData, 'account_identify') expect(payload.account_id).toEqual(fakeGroupData.account_id) + expect(payload.user_id).toEqual(fakeGroupData.user_id) expect(payload.traits.plan_name).toEqual(fakeGroupData.plan) expect(payload.traits.industry).toEqual(fakeGroupData.industry) expect(payload.traits.website).toEqual(fakeGroupData.website) expect(payload.traits).toHaveProperty('required') + expect(payload.local_tz_offset).toEqual(60) }) }) diff --git a/packages/destination-actions/src/destinations/hyperengage/group/generated-types.ts b/packages/destination-actions/src/destinations/hyperengage/group/generated-types.ts index 2529094d87..880ae08712 100644 --- a/packages/destination-actions/src/destinations/hyperengage/group/generated-types.ts +++ b/packages/destination-actions/src/destinations/hyperengage/group/generated-types.ts @@ -5,6 +5,10 @@ export interface Payload { * The External ID of the account to send properties for */ account_id: string + /** + * The ID associated with the user + */ + user_id?: string /** * The Account name */ diff --git a/packages/destination-actions/src/destinations/hyperengage/group/index.ts b/packages/destination-actions/src/destinations/hyperengage/group/index.ts index 26ae905724..54cf68aa2f 100644 --- a/packages/destination-actions/src/destinations/hyperengage/group/index.ts +++ b/packages/destination-actions/src/destinations/hyperengage/group/index.ts @@ -22,6 +22,12 @@ const action: ActionDefinition = { } } }, + user_id: { + type: 'string', + description: 'The ID associated with the user', + label: 'User ID', + default: { '@path': '$.userId' } + }, name: { type: 'string', required: true, @@ -41,7 +47,13 @@ const action: ActionDefinition = { description: 'The timestamp when the account was created, represented in the ISO-8601 date format. For instance, "2023-09-26T15:30:00Z".', label: 'Account created at', - default: { '@path': '$.traits.created_at' } + default: { + '@if': { + exists: { '@path': '$.traits.created_at' }, + then: { '@path': '$.traits.created_at' }, + else: { '@path': '$.traits.createdAt' } + } + } }, traits: { type: 'object', @@ -55,7 +67,13 @@ const action: ActionDefinition = { required: false, description: 'Subscription plan the account is associated with', label: 'Account subscription plan', - default: { '@path': '$.traits.plan' } + default: { + '@if': { + exists: { '@path': '$.traits.plan' }, + then: { '@path': '$.traits.plan' }, + else: { '@path': '$.traits.plan_name' } + } + } }, industry: { type: 'string', diff --git a/packages/destination-actions/src/destinations/hyperengage/identify/index.ts b/packages/destination-actions/src/destinations/hyperengage/identify/index.ts index 4acd8d5fa8..344143e373 100644 --- a/packages/destination-actions/src/destinations/hyperengage/identify/index.ts +++ b/packages/destination-actions/src/destinations/hyperengage/identify/index.ts @@ -91,7 +91,13 @@ const action: ActionDefinition = { description: 'The timestamp when the user was created, represented in the ISO-8601 date format. For instance, "2023-09-26T15:30:00Z".', label: 'Created at', - default: { '@path': '$.traits.created_at' } + default: { + '@if': { + exists: { '@path': '$.traits.created_at' }, + then: { '@path': '$.traits.created_at' }, + else: { '@path': '$.traits.createdAt' } + } + } }, traits: { type: 'object', diff --git a/packages/destination-actions/src/destinations/hyperengage/track/generated-types.ts b/packages/destination-actions/src/destinations/hyperengage/track/generated-types.ts index f05ca21a4a..c11f816ded 100644 --- a/packages/destination-actions/src/destinations/hyperengage/track/generated-types.ts +++ b/packages/destination-actions/src/destinations/hyperengage/track/generated-types.ts @@ -9,16 +9,16 @@ export interface Payload { * The user id, to uniquely identify the user associated with the event */ user_id: string + /** + * The account id, to uniquely identify the account associated with the user + */ + account_id?: string /** * The properties of the track call */ properties?: { [k: string]: unknown } - /** - * The account id, to uniquely identify the account associated with the user - */ - account_id?: string /** * User Anonymous id */ diff --git a/packages/destination-actions/src/destinations/hyperengage/track/index.ts b/packages/destination-actions/src/destinations/hyperengage/track/index.ts index 9565ede19c..495803544b 100644 --- a/packages/destination-actions/src/destinations/hyperengage/track/index.ts +++ b/packages/destination-actions/src/destinations/hyperengage/track/index.ts @@ -23,13 +23,6 @@ const action: ActionDefinition = { label: 'User id', default: { '@path': '$.userId' } }, - properties: { - type: 'object', - required: false, - description: 'The properties of the track call', - label: 'Event properties', - default: { '@path': '$.properties' } - }, account_id: { type: 'string', required: false, @@ -37,12 +30,19 @@ const action: ActionDefinition = { label: 'Account id', default: { '@if': { - exists: { '@path': '$.context.group_id' }, - then: { '@path': '$.context.group_id' }, + exists: { '@path': '$.context.groupId' }, + then: { '@path': '$.context.groupId' }, else: { '@path': '$.groupId' } } } }, + properties: { + type: 'object', + required: false, + description: 'The properties of the track call', + label: 'Event properties', + default: { '@path': '$.properties' } + }, ...commonFields }, perform: (request, data) => { diff --git a/packages/destination-actions/src/destinations/hyperengage/validateInput.ts b/packages/destination-actions/src/destinations/hyperengage/validateInput.ts index 770b52a320..0e22c727d8 100644 --- a/packages/destination-actions/src/destinations/hyperengage/validateInput.ts +++ b/packages/destination-actions/src/destinations/hyperengage/validateInput.ts @@ -32,8 +32,11 @@ export const validateInput = ( // Resolve local_tz_offset property, we can get local_tz_offset from the input context.timezone if (input?.timezone) { - const offset = new Date().toLocaleString('en-US', { timeZone: input.timezone, timeZoneName: 'short' }).split(' ')[2] - properties.local_tz_offset = offset + const offset = new Date() + .toLocaleString('en-US', { timeZone: input.timezone, timeZoneName: 'short' }) + .split(' ')[3] + .slice(3) + properties.local_tz_offset = parseInt(offset) * 60 delete properties.timezone } From 10cc2399e736e2975b62ba1257550b5de229c5d8 Mon Sep 17 00:00:00 2001 From: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 12:42:06 +0100 Subject: [PATCH 02/34] adding default paths for click id and cookie (#1729) --- .../snap-conversions-api/snap-capi-properties.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/snap-conversions-api/snap-capi-properties.ts b/packages/destination-actions/src/destinations/snap-conversions-api/snap-capi-properties.ts index 9b4a25ce01..b6722e48c8 100644 --- a/packages/destination-actions/src/destinations/snap-conversions-api/snap-capi-properties.ts +++ b/packages/destination-actions/src/destinations/snap-conversions-api/snap-capi-properties.ts @@ -175,7 +175,10 @@ export const uuid_c1: InputField = { label: 'uuid_c1 Cookie', description: 'Unique user ID cookie. If you are using the Pixel SDK, you can access a cookie1 by looking at the _scid value.', - type: 'string' + type: 'string', + default: { + '@path': '$.integrations.Snap Conversions Api.uuid_c1' + } } export const idfv: InputField = { @@ -351,7 +354,10 @@ export const click_id: InputField = { label: 'Click ID', description: "The ID value stored in the landing page URL's `&ScCid=` query parameter. Using this ID improves ad measurement performance. We also encourage advertisers who are using `click_id` to pass the full url in the `page_url` field. For more details, please refer to [Sending a Click ID](#sending-a-click-id)", - type: 'string' + type: 'string', + default: { + '@path': '$.integrations.Snap Conversions Api.click_id' + } } //Check to see what ids need to be passed depending on the event_conversion_type From 6fc66bebd2fc855c8d7865be2b8ad2868bc5ec5d Mon Sep 17 00:00:00 2001 From: Chris Brown Date: Wed, 29 Nov 2023 06:42:32 -0500 Subject: [PATCH 03/34] rename full_name to display_name to fix API changes (#1743) * rename full_name to display_name to fix API changes * Update full_name to display_name in tests --- .../src/destinations/devrev/createRevUser/index.ts | 2 +- .../src/destinations/devrev/mocks/index.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/devrev/createRevUser/index.ts b/packages/destination-actions/src/destinations/devrev/createRevUser/index.ts index 99667ffe98..528e741ad7 100644 --- a/packages/destination-actions/src/destinations/devrev/createRevUser/index.ts +++ b/packages/destination-actions/src/destinations/devrev/createRevUser/index.ts @@ -159,7 +159,7 @@ const action: ActionDefinition = { method: 'post', json: { email, - full_name: name, + display_name: name, external_ref: email, org_id: revOrgId } diff --git a/packages/destination-actions/src/destinations/devrev/mocks/index.ts b/packages/destination-actions/src/destinations/devrev/mocks/index.ts index 226e83c0e8..4468852dd1 100644 --- a/packages/destination-actions/src/destinations/devrev/mocks/index.ts +++ b/packages/destination-actions/src/destinations/devrev/mocks/index.ts @@ -3,7 +3,7 @@ import * as types from '../utils/types' interface revUserCreateBody { email: string - full_name: string + display_name: string org_id: string } @@ -124,7 +124,7 @@ export const revUsersCreateResponse = async (_: never, body: revUserCreateBody) rev_user: { id: testRevUserNewer.id, created_date: newerCreateDate, - display_name: body.full_name, + display_name: body.display_name, email: body.email, rev_org: { id: body.org_id, From ce3c0820fa3a600f040f02543f5f760bfa49cf7b Mon Sep 17 00:00:00 2001 From: maryamsharif <99763167+maryamsharif@users.noreply.github.com> Date: Wed, 29 Nov 2023 03:43:21 -0800 Subject: [PATCH 04/34] [STRATCONN-3376] Add phone number to TikTok Audiences (#1730) * Add phone number * Fix unit tests * Hide enable batching field + adjust names --- .../addToAudience/__tests__/index.test.ts | 62 ++++++++++++++++--- .../addToAudience/generated-types.ts | 8 +++ .../tiktok-audiences/addToAudience/index.ts | 4 ++ .../addUser/__tests__/index.test.ts | 55 ++++++++++++++-- .../addUser/generated-types.ts | 8 +++ .../tiktok-audiences/addUser/index.ts | 4 ++ .../tiktok-audiences/functions.ts | 22 ++++++- .../tiktok-audiences/properties.ts | 35 +++++++++-- .../__tests__/index.test.ts | 14 +++-- .../removeFromAudience/generated-types.ts | 8 +++ .../removeFromAudience/index.ts | 4 ++ .../removeUser/__tests__/index.test.ts | 14 +++-- .../removeUser/generated-types.ts | 8 +++ .../tiktok-audiences/removeUser/index.ts | 4 ++ 14 files changed, 221 insertions(+), 29 deletions(-) diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/__tests__/index.test.ts b/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/__tests__/index.test.ts index 503a7e35c0..38f40cc656 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/__tests__/index.test.ts @@ -29,7 +29,8 @@ const event = createTestEvent({ advertisingId: ADVERTISING_ID }, traits: { - email: 'testing@testing.com' + email: 'testing@testing.com', + phone: '1234567890' }, personas: { audience_settings: { @@ -42,7 +43,7 @@ const event = createTestEvent({ }) const updateUsersRequestBody = { - id_schema: ['EMAIL_SHA256', 'IDFA_SHA256'], + id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'], advertiser_ids: [ADVERTISER_ID], action: 'add', batch_data: [ @@ -51,6 +52,10 @@ const updateUsersRequestBody = { id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', audience_ids: [EXTERNAL_AUDIENCE_ID] }, + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: [EXTERNAL_AUDIENCE_ID] + }, { id: '0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6', audience_ids: [EXTERNAL_AUDIENCE_ID] @@ -74,6 +79,9 @@ describe('TiktokAudiences.addToAudience', () => { }) expect(r[0].status).toEqual(200) + expect(r[0].options.body).toMatchInlineSnapshot( + `"{\\"advertiser_ids\\":[\\"123\\"],\\"action\\":\\"add\\",\\"id_schema\\":[\\"EMAIL_SHA256\\",\\"PHONE_SHA256\\",\\"IDFA_SHA256\\"],\\"batch_data\\":[[{\\"id\\":\\"584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777\\",\\"audience_ids\\":[\\"12345\\"]},{\\"id\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\",\\"audience_ids\\":[\\"12345\\"]},{\\"id\\":\\"0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6\\",\\"audience_ids\\":[\\"12345\\"]}]]}"` + ) }) it('should normalize and hash emails correctly', async () => { @@ -101,7 +109,8 @@ describe('TiktokAudiences.addToAudience', () => { useDefaultMappings: true, auth, mapping: { - send_advertising_id: false + send_advertising_id: false, + send_phone: false } }) @@ -110,6 +119,39 @@ describe('TiktokAudiences.addToAudience', () => { ) }) + it('should normalize and hash phone correctly', async () => { + nock(`${BASE_URL}${TIKTOK_API_VERSION}`) + .post('/segment/mapping/', { + advertiser_ids: ['123'], + action: 'add', + id_schema: ['PHONE_SHA256'], + batch_data: [ + [ + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: [EXTERNAL_AUDIENCE_ID] + } + ] + ] + }) + .reply(200) + + const responses = await testDestination.testAction('addToAudience', { + event, + settings: { + advertiser_ids: ['123'] + }, + useDefaultMappings: true, + auth, + mapping: { + send_advertising_id: false, + send_email: false + } + }) + + expect(responses[0].options.body).toMatchInlineSnapshot() + }) + it('should fail if an audience id is invalid', async () => { const anotherEvent = createTestEvent({ event: 'Audience Entered', @@ -122,7 +164,8 @@ describe('TiktokAudiences.addToAudience', () => { advertisingId: ADVERTISING_ID }, traits: { - email: 'testing@testing.com' + email: 'testing@testing.com', + phone: '1234567890' }, personas: { audience_settings: { @@ -136,7 +179,7 @@ describe('TiktokAudiences.addToAudience', () => { nock(`${BASE_URL}${TIKTOK_API_VERSION}/segment/mapping/`) .post(/.*/, { - id_schema: ['EMAIL_SHA256', 'IDFA_SHA256'], + id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'], advertiser_ids: [ADVERTISER_ID], action: 'add', batch_data: [ @@ -145,6 +188,10 @@ describe('TiktokAudiences.addToAudience', () => { id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', audience_ids: ['THIS_ISNT_REAL'] }, + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: ['THIS_ISNT_REAL'] + }, { id: '0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6', audience_ids: ['THIS_ISNT_REAL'] @@ -182,10 +229,11 @@ describe('TiktokAudiences.addToAudience', () => { selected_advertiser_id: '123', audience_id: '123456', send_email: false, - send_advertising_id: false + send_advertising_id: false, + send_phone: false } }) - ).rejects.toThrow('At least one of `Send Email`, or `Send Advertising ID` must be set to `true`.') + ).rejects.toThrow('At least one of `Send Email`, `Send Phone` or `Send Advertising ID` must be set to `true`.') }) it('should fail if email and/or advertising_id is not in the payload', async () => { nock(`${BASE_URL}${TIKTOK_API_VERSION}/segment/mapping/`).post(/.*/, updateUsersRequestBody).reply(400) diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/generated-types.ts b/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/generated-types.ts index 87e5448708..df0e8160a2 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/generated-types.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/generated-types.ts @@ -5,6 +5,10 @@ export interface Payload { * The user's email address to send to TikTok. */ email?: string + /** + * The user's phone number to send to TikTok. + */ + phone?: string /** * The user's mobile advertising ID to send to TikTok. This could be a GAID, IDFA, or AAID */ @@ -13,6 +17,10 @@ export interface Payload { * Send email to TikTok. Segment will hash this value before sending */ send_email?: boolean + /** + * Send phone number to TikTok. Segment will hash this value before sending + */ + send_phone?: boolean /** * Send mobile advertising ID (IDFA, AAID or GAID) to TikTok. Segment will hash this value before sending. */ diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/index.ts b/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/index.ts index c387e45d8c..0b778a611c 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/addToAudience/index.ts @@ -5,7 +5,9 @@ import { processPayload } from '../functions' import { email, advertising_id, + phone, send_email, + send_phone, send_advertising_id, event_name, enable_batching, @@ -19,8 +21,10 @@ const action: ActionDefinition = { defaultSubscription: 'event = "Audience Entered"', fields: { email: { ...email }, + phone: { ...phone }, advertising_id: { ...advertising_id }, send_email: { ...send_email }, + send_phone: { ...send_phone }, send_advertising_id: { ...send_advertising_id }, event_name: { ...event_name }, enable_batching: { ...enable_batching }, diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/addUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/tiktok-audiences/addUser/__tests__/index.test.ts index c93fcd9720..6b220836a9 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/addUser/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/addUser/__tests__/index.test.ts @@ -26,7 +26,8 @@ const event = createTestEvent({ advertisingId: '123' }, traits: { - email: 'testing@testing.com' + email: 'testing@testing.com', + phone: '1234567890' } } }) @@ -34,13 +35,17 @@ const event = createTestEvent({ const updateUsersRequestBody = { advertiser_ids: ['123'], action: 'add', - id_schema: ['EMAIL_SHA256', 'IDFA_SHA256'], + id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'], batch_data: [ [ { id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', audience_ids: ['1234345'] }, + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: ['1234345'] + }, { id: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', audience_ids: ['1234345'] @@ -94,7 +99,8 @@ describe('TiktokAudiences.addUser', () => { mapping: { selected_advertiser_id: '123', audience_id: '1234345', - send_advertising_id: false + send_advertising_id: false, + send_phone: false } }) expect(responses[0].options.body).toMatchInlineSnapshot( @@ -102,6 +108,41 @@ describe('TiktokAudiences.addUser', () => { ) }) + it('should normalize and hash phone correctly', async () => { + nock(`${BASE_URL}${TIKTOK_API_VERSION}/segment/mapping/`) + .post(/.*/, { + advertiser_ids: ['123'], + action: 'add', + id_schema: ['PHONE_SHA256'], + batch_data: [ + [ + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: ['1234345'] + } + ] + ] + }) + .reply(200) + const responses = await testDestination.testAction('addUser', { + event, + settings: { + advertiser_ids: ['123'] + }, + useDefaultMappings: true, + auth, + mapping: { + selected_advertiser_id: '123', + audience_id: '1234345', + send_advertising_id: false, + send_email: false + } + }) + expect(responses[0].options.body).toMatchInlineSnapshot( + `"{\\"advertiser_ids\\":[\\"123\\"],\\"action\\":\\"add\\",\\"id_schema\\":[\\"PHONE_SHA256\\"],\\"batch_data\\":[[{\\"id\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\",\\"audience_ids\\":[\\"1234345\\"]}]]}"` + ) + }) + it('should fail if an audience id is invalid', async () => { nock(`${BASE_URL}${TIKTOK_API_VERSION}/segment/mapping/`).post(/.*/, updateUsersRequestBody).reply(400) @@ -136,10 +177,11 @@ describe('TiktokAudiences.addUser', () => { selected_advertiser_id: '123', audience_id: '123456', send_email: false, - send_advertising_id: false + send_advertising_id: false, + send_phone: false } }) - ).rejects.toThrow('At least one of `Send Email`, or `Send Advertising ID` must be set to `true`.') + ).rejects.toThrow('At least one of `Send Email`, `Send Phone` or `Send Advertising ID` must be set to `true`.') }) it('should fail if email and/or advertising_id is not in the payload', async () => { nock(`${BASE_URL}${TIKTOK_API_VERSION}/segment/mapping/`).post(/.*/, updateUsersRequestBody).reply(400) @@ -159,7 +201,8 @@ describe('TiktokAudiences.addUser', () => { selected_advertiser_id: '123', audience_id: 'personas_test_audience', send_email: true, - send_advertising_id: true + send_advertising_id: true, + send_phone: true } }) ).rejects.toThrowError('At least one of Email Id or Advertising ID must be provided.') diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/addUser/generated-types.ts b/packages/destination-actions/src/destinations/tiktok-audiences/addUser/generated-types.ts index ac0bb830f7..444db36fe4 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/addUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/addUser/generated-types.ts @@ -13,6 +13,10 @@ export interface Payload { * The user's email address to send to TikTok. */ email?: string + /** + * The user's phone number to send to TikTok. + */ + phone?: string /** * The user's mobile advertising ID to send to TikTok. This could be a GAID, IDFA, or AAID */ @@ -21,6 +25,10 @@ export interface Payload { * Send email to TikTok. Segment will hash this value before sending */ send_email?: boolean + /** + * Send phone number to TikTok. Segment will hash this value before sending + */ + send_phone?: boolean /** * Send mobile advertising ID (IDFA, AAID or GAID) to TikTok. Segment will hash this value before sending. */ diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/addUser/index.ts b/packages/destination-actions/src/destinations/tiktok-audiences/addUser/index.ts index 444c236f46..21597bb459 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/addUser/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/addUser/index.ts @@ -6,8 +6,10 @@ import { selected_advertiser_id, audience_id, email, + phone, advertising_id, send_email, + send_phone, send_advertising_id, event_name, enable_batching @@ -26,8 +28,10 @@ const action: ActionDefinition = { selected_advertiser_id: { ...selected_advertiser_id }, audience_id: { ...audience_id }, email: { ...email }, + phone: { ...phone }, advertising_id: { ...advertising_id }, send_email: { ...send_email }, + send_phone: { ...send_phone }, send_advertising_id: { ...send_advertising_id }, event_name: { ...event_name }, enable_batching: { ...enable_batching } diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/functions.ts b/packages/destination-actions/src/destinations/tiktok-audiences/functions.ts index 25d3934522..b71fa61b1d 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/functions.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/functions.ts @@ -69,9 +69,13 @@ export async function createAudience( } export function validate(payloads: GenericPayload[]): void { - if (payloads[0].send_email === false && payloads[0].send_advertising_id === false) { + if ( + payloads[0].send_email === false && + payloads[0].send_advertising_id === false && + payloads[0].send_phone === false + ) { throw new IntegrationError( - 'At least one of `Send Email`, or `Send Advertising ID` must be set to `true`.', + 'At least one of `Send Email`, `Send Phone` or `Send Advertising ID` must be set to `true`.', 'INVALID_SETTINGS', 400 ) @@ -85,6 +89,9 @@ export function getIDSchema(payload: GenericPayload): string[] { if (payload.send_email === true) { id_schema.push('EMAIL_SHA256') } + if (payload.send_phone === true) { + id_schema.push('PHONE_SHA256') + } if (payload.send_advertising_id === true) { id_schema.push('IDFA_SHA256') } @@ -127,6 +134,17 @@ export function extractUsers(payloads: GenericPayload[]): {}[][] { user_ids.push(email_id) } + if (payload.send_phone === true) { + let phone_id = {} + if (payload.phone) { + phone_id = { + id: createHash('sha256').update(payload.phone).digest('hex'), + audience_ids: [external_audience_id] + } + } + user_ids.push(phone_id) + } + if (payload.send_advertising_id === true) { let advertising_id = {} if (payload.advertising_id) { diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/properties.ts b/packages/destination-actions/src/destinations/tiktok-audiences/properties.ts index 74391c115b..c8697360f9 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/properties.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/properties.ts @@ -43,31 +43,53 @@ export const email: InputField = { label: 'User Email', description: "The user's email address to send to TikTok.", type: 'string', - unsafe_hidden: true, // This field is hidden from customers because the desired value always appears at path '$.context.traits.email' in Personas events. default: { - '@path': '$.context.traits.email' + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } } } export const send_email: InputField = { - label: 'Send Email', + label: 'Send Email?', description: 'Send email to TikTok. Segment will hash this value before sending', type: 'boolean', default: true } +export const phone: InputField = { + label: 'User Phone Number', + description: "The user's phone number to send to TikTok.", + type: 'string', + default: { + '@if': { + exists: { '@path': '$.context.traits.phone' }, + then: { '@path': '$.context.traits.phone' }, + else: { '@path': '$.properties.phone' } + } + } +} + +export const send_phone: InputField = { + label: 'Send Phone Number?', + description: 'Send phone number to TikTok. Segment will hash this value before sending', + type: 'boolean', + default: true +} + export const advertising_id: InputField = { label: 'User Advertising ID', description: "The user's mobile advertising ID to send to TikTok. This could be a GAID, IDFA, or AAID", type: 'string', - unsafe_hidden: true, // This field is hidden from customers because the desired value always appears at path '$.context.device.advertisingId' in Personas events. default: { '@path': '$.context.device.advertisingId' } } export const send_advertising_id: InputField = { - label: 'Send Mobile Advertising ID', + label: 'Send Mobile Advertising ID?', description: 'Send mobile advertising ID (IDFA, AAID or GAID) to TikTok. Segment will hash this value before sending.', type: 'boolean', @@ -88,7 +110,8 @@ export const enable_batching: InputField = { label: 'Enable Batching', description: 'Enable batching of requests to the TikTok Audiences.', type: 'boolean', - default: true + default: true, + unsafe_hidden: true } export const external_audience_id: InputField = { diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/__tests__/index.test.ts b/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/__tests__/index.test.ts index d797860d9f..89b33a36d4 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/__tests__/index.test.ts @@ -29,7 +29,8 @@ const event = createTestEvent({ advertisingId: ADVERTISING_ID }, traits: { - email: 'testing@testing.com' + email: 'testing@testing.com', + phone: '1234567890' }, personas: { audience_settings: { @@ -42,7 +43,7 @@ const event = createTestEvent({ }) const updateUsersRequestBody = { - id_schema: ['EMAIL_SHA256', 'IDFA_SHA256'], + id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'], advertiser_ids: [ADVERTISER_ID], action: 'delete', batch_data: [ @@ -51,6 +52,10 @@ const updateUsersRequestBody = { id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', audience_ids: [EXTERNAL_AUDIENCE_ID] }, + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: [EXTERNAL_AUDIENCE_ID] + }, { id: '0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6', audience_ids: [EXTERNAL_AUDIENCE_ID] @@ -154,9 +159,10 @@ describe('TiktokAudiences.removeFromAudience', () => { selected_advertiser_id: '123', audience_id: '123456', send_email: false, - send_advertising_id: false + send_advertising_id: false, + send_phone: false } }) - ).rejects.toThrow('At least one of `Send Email`, or `Send Advertising ID` must be set to `true`.') + ).rejects.toThrow('At least one of `Send Email`, `Send Phone` or `Send Advertising ID` must be set to `true`.') }) }) diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/generated-types.ts b/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/generated-types.ts index 87e5448708..df0e8160a2 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/generated-types.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/generated-types.ts @@ -5,6 +5,10 @@ export interface Payload { * The user's email address to send to TikTok. */ email?: string + /** + * The user's phone number to send to TikTok. + */ + phone?: string /** * The user's mobile advertising ID to send to TikTok. This could be a GAID, IDFA, or AAID */ @@ -13,6 +17,10 @@ export interface Payload { * Send email to TikTok. Segment will hash this value before sending */ send_email?: boolean + /** + * Send phone number to TikTok. Segment will hash this value before sending + */ + send_phone?: boolean /** * Send mobile advertising ID (IDFA, AAID or GAID) to TikTok. Segment will hash this value before sending. */ diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/index.ts b/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/index.ts index 09f6f9ac73..077222e2e1 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/removeFromAudience/index.ts @@ -4,7 +4,9 @@ import type { Payload } from './generated-types' import { processPayload } from '../functions' import { email, + phone, send_email, + send_phone, send_advertising_id, advertising_id, event_name, @@ -19,8 +21,10 @@ const action: ActionDefinition = { defaultSubscription: 'event = "Audience Exited"', fields: { email: { ...email }, + phone: { ...phone }, advertising_id: { ...advertising_id }, send_email: { ...send_email }, + send_phone: { ...send_phone }, send_advertising_id: { ...send_advertising_id }, event_name: { ...event_name }, enable_batching: { ...enable_batching }, diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/__tests__/index.test.ts b/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/__tests__/index.test.ts index 114ad04c54..9edc219614 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/__tests__/index.test.ts @@ -26,7 +26,8 @@ const event = createTestEvent({ advertisingId: '123' }, traits: { - email: 'testing@testing.com' + email: 'testing@testing.com', + phone: '1234567890' } } }) @@ -34,13 +35,17 @@ const event = createTestEvent({ const updateUsersRequestBody = { advertiser_ids: ['123'], action: 'delete', - id_schema: ['EMAIL_SHA256', 'IDFA_SHA256'], + id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'], batch_data: [ [ { id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777', audience_ids: ['1234345'] }, + { + id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646', + audience_ids: ['1234345'] + }, { id: 'a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3', audience_ids: ['1234345'] @@ -103,9 +108,10 @@ describe('TiktokAudiences.removeUser', () => { selected_advertiser_id: '123', audience_id: '123456', send_email: false, - send_advertising_id: false + send_advertising_id: false, + send_phone: false } }) - ).rejects.toThrow('At least one of `Send Email`, or `Send Advertising ID` must be set to `true`.') + ).rejects.toThrow('At least one of `Send Email`, `Send Phone` or `Send Advertising ID` must be set to `true`.') }) }) diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/generated-types.ts b/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/generated-types.ts index ac0bb830f7..444db36fe4 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/generated-types.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/generated-types.ts @@ -13,6 +13,10 @@ export interface Payload { * The user's email address to send to TikTok. */ email?: string + /** + * The user's phone number to send to TikTok. + */ + phone?: string /** * The user's mobile advertising ID to send to TikTok. This could be a GAID, IDFA, or AAID */ @@ -21,6 +25,10 @@ export interface Payload { * Send email to TikTok. Segment will hash this value before sending */ send_email?: boolean + /** + * Send phone number to TikTok. Segment will hash this value before sending + */ + send_phone?: boolean /** * Send mobile advertising ID (IDFA, AAID or GAID) to TikTok. Segment will hash this value before sending. */ diff --git a/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/index.ts b/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/index.ts index 131ef737da..dd64a7b6a9 100644 --- a/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/index.ts +++ b/packages/destination-actions/src/destinations/tiktok-audiences/removeUser/index.ts @@ -6,7 +6,9 @@ import { selected_advertiser_id, audience_id, email, + phone, send_email, + send_phone, send_advertising_id, advertising_id, event_name, @@ -26,8 +28,10 @@ const action: ActionDefinition = { selected_advertiser_id: { ...selected_advertiser_id }, audience_id: { ...audience_id }, email: { ...email }, + phone: { ...phone }, advertising_id: { ...advertising_id }, send_email: { ...send_email }, + send_phone: { ...send_phone }, send_advertising_id: { ...send_advertising_id }, event_name: { ...event_name }, enable_batching: { ...enable_batching } From 593b02437c6ee6ba31bd89f1e231e2033e4afe9b Mon Sep 17 00:00:00 2001 From: Sayan Das <109198085+sayan-das-in@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:14:02 +0530 Subject: [PATCH 05/34] LR: Fix CSV generator to account for all rows (#1735) * Fixed CSV generator to account for all rows * Updated CSV processor, tests and snapshots --- .../__snapshots__/snapshot.test.ts.snap | 1211 +---------------- .../__tests__/operations.test.ts | 90 ++ .../liveramp-audiences/operations.ts | 43 +- 3 files changed, 178 insertions(+), 1166 deletions(-) create mode 100644 packages/destination-actions/src/destinations/liveramp-audiences/__tests__/operations.test.ts diff --git a/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/__snapshots__/snapshot.test.ts.snap index b0b90d1c8c..ba18b90da5 100644 --- a/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,73 +1,73 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Testing snapshot for LiverampAudiences's audienceEnteredS3 destination action: all fields 1`] = ` -"audience_key,testType,testType -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\"" +"audience_key,testType +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\"" `; exports[`Testing snapshot for LiverampAudiences's audienceEnteredS3 destination action: missing minimum payload size 1`] = `[PayloadValidationError: received payload count below LiveRamp's ingestion limits. expected: >=25 actual: 1]`; exports[`Testing snapshot for LiverampAudiences's audienceEnteredS3 destination action: required fields 1`] = ` -"audience_key,testType,testType -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" -\\"8I!3YmPiv2%lv7\\",\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\"" +"audience_key,testType +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\" +\\"8I!3YmPiv2%lv7\\",\\"047e87c5aeaa6844b0125de813f7683f636401c49b092afd324d2c32d21ccaff\\"" `; exports[`Testing snapshot for LiverampAudiences's audienceEnteredS3 destination action: required fields 2`] = ` Headers { Symbol(map): Object { "authorization": Array [ - "AWS4-HMAC-SHA256 Credential=12345/19700101/us-west/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date, Signature=4a5564353d61d819ae9759a627f4a4e9e0fd4acde988984d3220c391f0abde5e", + "AWS4-HMAC-SHA256 Credential=12345/19700101/us-west/s3/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date, Signature=4d3fa5773e208c949b4c6278e1eb76fdc88ddb4c9ebece9ffacf3e40e1ff2a51", ], "content-length": Array [ - "2555", + "2121", ], "content-type": Array [ "application/x-www-form-urlencoded; charset=utf-8", @@ -79,7 +79,7 @@ Headers { "Segment (Actions)", ], "x-amz-content-sha256": Array [ - "7fced28a29b028b75193be3b3824c4df2fcc96ba80b74a741afc069f54bd6156", + "a8ed2a4ab5e742f5669c17ebc3f5cb0f79dc29fd08c23aa41a1e27a8c339b957", ], "x-amz-date": Array [ "19700101T000012Z", @@ -114,15 +114,6 @@ Array [ 121, 112, 101, - 44, - 116, - 101, - 115, - 116, - 84, - 121, - 112, - 101, 10, 34, 105, @@ -139,20 +130,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -234,20 +211,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -329,20 +292,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -424,20 +373,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -519,20 +454,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -614,20 +535,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -709,20 +616,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -804,20 +697,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -899,20 +778,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -994,20 +859,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1089,20 +940,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1184,20 +1021,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1279,20 +1102,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1374,20 +1183,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1469,20 +1264,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1564,20 +1345,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1659,20 +1426,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1754,20 +1507,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1849,20 +1588,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -1944,20 +1669,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2039,20 +1750,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2134,20 +1831,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2210,24 +1893,10 @@ Array [ 53, 101, 100, - 56, - 51, - 34, - 10, - 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, + 56, + 51, 34, - 44, + 10, 34, 105, 50, @@ -2324,20 +1993,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2419,20 +2074,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2527,15 +2168,6 @@ Array [ 121, 112, 101, - 44, - 116, - 101, - 115, - 116, - 84, - 121, - 112, - 101, 10, 34, 105, @@ -2552,20 +2184,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2647,20 +2265,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2742,20 +2346,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2837,20 +2427,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -2932,20 +2508,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3027,20 +2589,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3122,20 +2670,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3217,20 +2751,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3312,20 +2832,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3407,20 +2913,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3502,20 +2994,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3597,20 +3075,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3692,20 +3156,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3787,20 +3237,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -3860,27 +3296,13 @@ Array [ 55, 102, 52, - 53, - 101, - 100, - 56, - 51, - 34, - 10, - 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, + 53, + 101, + 100, + 56, + 51, 34, - 44, + 10, 34, 105, 50, @@ -3977,20 +3399,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4072,20 +3480,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4167,20 +3561,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4262,20 +3642,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4357,20 +3723,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4452,20 +3804,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4547,20 +3885,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4642,20 +3966,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4737,20 +4047,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4832,20 +4128,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -4947,15 +4229,6 @@ Array [ 121, 112, 101, - 44, - 116, - 101, - 115, - 116, - 84, - 121, - 112, - 101, 10, 34, 105, @@ -4972,20 +4245,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5067,20 +4326,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5162,20 +4407,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5257,20 +4488,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5352,20 +4569,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5447,20 +4650,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5523,24 +4712,10 @@ Array [ 53, 101, 100, - 56, - 51, - 34, - 10, - 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, + 56, + 51, 34, - 44, + 10, 34, 105, 50, @@ -5637,20 +4812,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5732,20 +4893,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5827,20 +4974,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -5922,20 +5055,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6017,20 +5136,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6112,20 +5217,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6207,20 +5298,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6302,20 +5379,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6397,20 +5460,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6492,20 +5541,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6587,20 +5622,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6682,20 +5703,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6777,20 +5784,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6872,20 +5865,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -6967,20 +5946,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -7062,20 +6027,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -7157,20 +6108,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, @@ -7252,20 +6189,6 @@ Array [ 34, 44, 34, - 105, - 50, - 57, - 57, - 56, - 41, - 66, - 97, - 77, - 111, - 122, - 34, - 44, - 34, 50, 102, 54, diff --git a/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/operations.test.ts b/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/operations.test.ts new file mode 100644 index 0000000000..59b601f98c --- /dev/null +++ b/packages/destination-actions/src/destinations/liveramp-audiences/__tests__/operations.test.ts @@ -0,0 +1,90 @@ +import { generateFile } from '../operations' +import type { Payload } from '../audienceEnteredSftp/generated-types' + +describe(`Test operations helper functions:`, () => { + it('should generate CSV with hashed and unhashed identifier data', async () => { + const payloads: Payload[] = [ + // Entry with hashed identifier data + { + audience_key: 'aud001', + delimiter: ',', + identifier_data: { + email: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' + }, + filename: 'test_file_name.csv', + enable_batching: true + }, + // Entry with unhashed identifier data + { + audience_key: 'aud002', + delimiter: ',', + unhashed_identifier_data: { + email: 'test@example.com' + }, + filename: 'test_file_name.csv', + enable_batching: true + }, + // Entry with both hashed and unhashed identifier data + { + audience_key: 'aud003', + delimiter: ',', + identifier_data: { + email: '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' + }, + unhashed_identifier_data: { + email: 'test@example.com' + }, + filename: 'test_file_name.csv', + enable_batching: true + } + ] + + const result = generateFile(payloads) + + const expectedFileContents = `audience_key,email\n"aud001","973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b"\n"aud002","973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b"\n"aud003","973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b"` + + expect(result).toMatchObject({ + filename: 'test_file_name.csv', + fileContents: Buffer.from(expectedFileContents) + }) + }) + + it('should generate CSV even if rows have missing data', async () => { + const payloads: Payload[] = [ + { + audience_key: 'aud001', + delimiter: ',', + filename: 'test_file_name.csv', + enable_batching: true + }, + { + audience_key: 'aud002', + delimiter: ',', + unhashed_identifier_data: { + email: 'test@example.com' + }, + filename: 'test_file_name.csv', + enable_batching: true + }, + { + audience_key: 'aud003', + delimiter: ',', + unhashed_identifier_data: { + email: 'test@example.com', + example_identifier: 'example-id' + }, + filename: 'test_file_name.csv', + enable_batching: true + } + ] + + const result = generateFile(payloads) + + const expectedFileContents = `audience_key,email,example_identifier\n"aud001"\n"aud002","973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b"\n"aud003","973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b","66a0acf498240ea61ce3ce698c5a30eb6824242b39695f8689d7c32499c79748"` + + expect(result).toMatchObject({ + filename: 'test_file_name.csv', + fileContents: Buffer.from(expectedFileContents) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/liveramp-audiences/operations.ts b/packages/destination-actions/src/destinations/liveramp-audiences/operations.ts index de59b96dcf..ab519399cf 100644 --- a/packages/destination-actions/src/destinations/liveramp-audiences/operations.ts +++ b/packages/destination-actions/src/destinations/liveramp-audiences/operations.ts @@ -32,45 +32,44 @@ Generates the LiveRamp ingestion file. Expected format: liveramp_audience_key[1],identifier_data[0..n] */ function generateFile(payloads: s3Payload[] | sftpPayload[]) { - const headers: string[] = ['audience_key'] + // Using a Set to keep track of headers + const headers = new Set() + headers.add('audience_key') - // Prepare header row - if (payloads[0].identifier_data) { - for (const identifier of Object.getOwnPropertyNames(payloads[0].identifier_data)) { - headers.push(identifier) - } - } - - if (payloads[0].unhashed_identifier_data) { - for (const identifier of Object.getOwnPropertyNames(payloads[0].unhashed_identifier_data)) { - headers.push(identifier) - } - } - - let rows = Buffer.from(headers.join(payloads[0].delimiter) + '\n') + // Declare rows as an empty Buffer + let rows = Buffer.from('') // Prepare data rows for (let i = 0; i < payloads.length; i++) { const payload = payloads[i] const row: string[] = [enquoteIdentifier(payload.audience_key)] - if (payload.identifier_data) { - for (const key in payload.identifier_data) { - if (Object.prototype.hasOwnProperty.call(payload.identifier_data, key)) { - row.push(enquoteIdentifier(String(payload.identifier_data[key]))) - } - } - } + // Process unhashed_identifier_data first if (payload.unhashed_identifier_data) { for (const key in payload.unhashed_identifier_data) { if (Object.prototype.hasOwnProperty.call(payload.unhashed_identifier_data, key)) { + headers.add(key) row.push(`"${hash(normalize(key, String(payload.unhashed_identifier_data[key])))}"`) } } } + + // Process identifier_data, skipping keys that have already been processed + if (payload.identifier_data) { + for (const key in payload.identifier_data) { + if (Object.prototype.hasOwnProperty.call(payload.identifier_data, key) && !headers.has(key)) { + headers.add(key) + row.push(enquoteIdentifier(String(payload.identifier_data[key]))) + } + } + } + rows = Buffer.concat([rows, Buffer.from(row.join(payload.delimiter) + (i + 1 === payloads.length ? '' : '\n'))]) } + // Add headers to the beginning of the file contents + rows = Buffer.concat([Buffer.from(Array.from(headers).join(payloads[0].delimiter) + '\n'), rows]) + const filename = payloads[0].filename return { filename, fileContents: rows } } From 31e207c8ed6cd738dfa813cccfc5241c3ea84d5f Mon Sep 17 00:00:00 2001 From: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Date: Wed, 29 Nov 2023 05:44:38 -0600 Subject: [PATCH 06/34] Channels-973: parse text field of action buttons (#1738) * feat: parse merge tags in action button text * chore: added unit test --- .../engage/twilio/__tests__/send-mobile-push.test.ts | 6 +++--- .../destinations/engage/twilio/sendMobilePush/PushSender.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-mobile-push.test.ts b/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-mobile-push.test.ts index c3703158af..a8cf2753a4 100644 --- a/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-mobile-push.test.ts +++ b/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-mobile-push.test.ts @@ -350,7 +350,7 @@ describe('sendMobilePush action', () => { expect(responses[0].data).toMatchObject(externalIds[0]) }) - it('parses links in tapActionButtons', async () => { + it('parses links and titles in tapActionButtons', async () => { const title = 'buy' const body = 'now' const tapAction = 'OPEN_DEEP_LINK' @@ -365,7 +365,7 @@ describe('sendMobilePush action', () => { tapActionButtons: [ { id: '1', - text: 'open', + text: 'open{{profile.traits.fav_color}}', onTap: 'deep_link', link: 'app://buy-now/{{profile.traits.fav_color}}' }, @@ -414,7 +414,7 @@ describe('sendMobilePush action', () => { tapActionButtons: [ { id: '1', - text: 'open', + text: 'openmantis_green', onTap: 'deep_link', link: 'app://buy-now/mantis_green' }, diff --git a/packages/destination-actions/src/destinations/engage/twilio/sendMobilePush/PushSender.ts b/packages/destination-actions/src/destinations/engage/twilio/sendMobilePush/PushSender.ts index 2b17604172..b5277b1a4d 100644 --- a/packages/destination-actions/src/destinations/engage/twilio/sendMobilePush/PushSender.ts +++ b/packages/destination-actions/src/destinations/engage/twilio/sendMobilePush/PushSender.ts @@ -120,7 +120,7 @@ export class PushSender extends TwilioMessageSender { return { ...button, onTap: this.getTapActionPreset(button.onTap, button.link), - ...(await this.parseContent({ link: button.link }, profile)) + ...(await this.parseContent({ link: button.link, text: button.text }, profile)) } }) ) From ac78bc78d6726ef8aab1210f8c3ad82aa1d9f9bf Mon Sep 17 00:00:00 2001 From: alfrimpong <119889384+alfrimpong@users.noreply.github.com> Date: Wed, 29 Nov 2023 05:50:15 -0600 Subject: [PATCH 07/34] Inc-7055 long term fix: add external ids to message tags (#1744) * feat: add external id type & value to message tags * feat: add test to enforce passthrough --- .../engage/twilio/__tests__/send-sms.test.ts | 66 +++++++++++-------- .../twilio/__tests__/send-whatsapp.test.ts | 21 ++++-- .../engage/twilio/utils/PhoneMessageSender.ts | 4 +- 3 files changed, 56 insertions(+), 35 deletions(-) diff --git a/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-sms.test.ts b/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-sms.test.ts index f92a894f88..c50686aa76 100644 --- a/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-sms.test.ts +++ b/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-sms.test.ts @@ -2,7 +2,11 @@ import nock from 'nock' import { createTestAction, expectErrorLogged, expectInfoLogged, loggerMock as logger } from './__helpers__/test-utils' import { FLAGON_NAME_LOG_ERROR, FLAGON_NAME_LOG_INFO, SendabilityStatus } from '../../utils' -const defaultTags = JSON.stringify({}) +const phoneNumber = '+1234567891' +const defaultTags = JSON.stringify({ + external_id_type: 'phone', + external_id_value: phoneNumber +}) describe.each(['stage', 'production'])('%s environment', (environment) => { const contentSid = 'g' @@ -19,7 +23,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { traitEnrichment: true, externalIds: [ { type: 'email', id: 'test@twilio.com', subscriptionStatus: 'true' }, - { type: 'phone', id: '+1234567891', subscriptionStatus: 'true', channelType: 'sms' } + { type: 'phone', id: phoneNumber, subscriptionStatus: 'true', channelType: 'sms' } ], sendBasedOnOptOut: false }) @@ -158,7 +162,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -205,7 +209,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', MediaUrl: 'http://myimg.com', Tags: defaultTags @@ -239,7 +243,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true' }) @@ -274,9 +278,12 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: '+1 (505) 555-4555', ShortenUrls: 'true', - Tags: defaultTags + Tags: JSON.stringify({ + external_id_type: 'phone', + external_id_value: '+1 (505) 555-4555' + }) }) const twilioHostname = 'api.nottwilio.com' @@ -285,7 +292,12 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { .post('/Messages.json', expectedTwilioRequest.toString()) .reply(201, {}) - const responses = await testAction({ settingsOverrides: { twilioHostname } }) + const responses = await testAction({ + settingsOverrides: { twilioHostname }, + mappingOverrides: { + externalIds: [{ type: 'phone', id: '+1 (505) 555-4555', subscriptionStatus: true, channelType: 'sms' }] + } + }) expect(responses.map((response) => response.url)).toStrictEqual([ `https://${twilioHostname}/2010-04-01/Accounts/a/Messages.json` ]) @@ -296,7 +308,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags, StatusCallback: @@ -351,7 +363,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -381,7 +393,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -392,7 +404,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'sms' }] + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'sms' }] } }) expect(responses.map((response) => response.url)).toStrictEqual([ @@ -407,7 +419,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -418,7 +430,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'sms' }], + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'sms' }], sendBasedOnOptOut: true } }) @@ -435,7 +447,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -446,7 +458,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'sms' }], + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'sms' }], sendBasedOnOptOut: undefined } }) @@ -463,7 +475,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -474,7 +486,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'sms' }] + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'sms' }] } }) expect(responses).toHaveLength(0) @@ -488,7 +500,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -499,7 +511,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'sms' }], + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'sms' }], sendBasedOnOptOut: undefined } }) @@ -515,7 +527,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -526,7 +538,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'sms' }], + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'sms' }], sendBasedOnOptOut: true } }) @@ -541,7 +553,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -552,7 +564,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus: randomSubscriptionStatusPhrase }] + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus: randomSubscriptionStatusPhrase }] } }) expect(responses).toHaveLength(0) @@ -636,7 +648,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', Tags: defaultTags }) @@ -662,9 +674,9 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const expectedTwilioRequest = new URLSearchParams({ Body: 'Hello world, jane!', From: 'MG1111222233334444', - To: '+1234567891', + To: phoneNumber, ShortenUrls: 'true', - Tags: '{"audience_id":"1","correlation_id":"1","journey_name":"j-1","step_name":"2","campaign_name":"c-3","campaign_key":"4","user_id":"u-5","message_id":"m-6"}' + Tags: '{"audience_id":"1","correlation_id":"1","journey_name":"j-1","step_name":"2","campaign_name":"c-3","campaign_key":"4","user_id":"u-5","message_id":"m-6","external_id_type":"phone","external_id_value":"+1234567891"}' }) const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') .post('/Messages.json', expectedTwilioRequest.toString()) diff --git a/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-whatsapp.test.ts b/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-whatsapp.test.ts index 60fe0e5dc9..b0751a34b3 100644 --- a/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-whatsapp.test.ts +++ b/packages/destination-actions/src/destinations/engage/twilio/__tests__/send-whatsapp.test.ts @@ -3,7 +3,11 @@ import { createTestAction, expectErrorLogged, expectInfoLogged } from './__helpe const defaultTemplateSid = 'my_template' const defaultTo = 'whatsapp:+1234567891' -const defaultTags = JSON.stringify({}) +const phoneNumber = '+1234567891' +const defaultTags = JSON.stringify({ + external_id_type: 'phone', + external_id_value: phoneNumber +}) describe.each(['stage', 'production'])('%s environment', (environment) => { const spaceId = 'd' @@ -19,7 +23,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { traitEnrichment: true, externalIds: [ { type: 'email', id: 'test@twilio.com', subscriptionStatus: 'subscribed' }, - { type: 'phone', id: '+1234567891', subscriptionStatus: 'subscribed', channelType: 'whatsapp' } + { type: 'phone', id: phoneNumber, subscriptionStatus: 'subscribed', channelType: 'whatsapp' } ] }) }) @@ -38,7 +42,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { it('should abort when there is no `channelType` in the external ID payload', async () => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus: 'subscribed' }] + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus: 'subscribed' }] } }) @@ -137,7 +141,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'whatsapp' }] + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'whatsapp' }] } }) expect(responses.map((response) => response.url)).toStrictEqual([ @@ -162,7 +166,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { const responses = await testAction({ mappingOverrides: { - externalIds: [{ type: 'phone', id: '+1234567891', subscriptionStatus, channelType: 'whatsapp' }] + externalIds: [{ type: 'phone', id: phoneNumber, subscriptionStatus, channelType: 'whatsapp' }] } }) expect(responses).toHaveLength(0) @@ -188,7 +192,7 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { externalIds: [ { type: 'phone', - id: '+1234567891', + id: phoneNumber, subscriptionStatus: randomSubscriptionStatusPhrase, channelType: 'whatsapp' } @@ -205,7 +209,10 @@ describe.each(['stage', 'production'])('%s environment', (environment) => { ContentSid: defaultTemplateSid, From: from, To: 'whatsapp:+19195551234', - Tags: defaultTags + Tags: JSON.stringify({ + external_id_type: 'phone', + external_id_value: '(919) 555 1234' + }) }) const twilioRequest = nock('https://api.twilio.com/2010-04-01/Accounts/a') diff --git a/packages/destination-actions/src/destinations/engage/twilio/utils/PhoneMessageSender.ts b/packages/destination-actions/src/destinations/engage/twilio/utils/PhoneMessageSender.ts index a0ea5b37aa..802565e7d9 100644 --- a/packages/destination-actions/src/destinations/engage/twilio/utils/PhoneMessageSender.ts +++ b/packages/destination-actions/src/destinations/engage/twilio/utils/PhoneMessageSender.ts @@ -48,7 +48,9 @@ export abstract class PhoneMessageSender ex campaign_name: this.payload.customArgs && this.payload.customArgs['campaign_name'], campaign_key: this.payload.customArgs && this.payload.customArgs['campaign_key'], user_id: this.payload.customArgs && this.payload.customArgs['user_id'], - message_id: this.payload.customArgs && this.payload.customArgs['message_id'] + message_id: this.payload.customArgs && this.payload.customArgs['message_id'], + external_id_type: recepient.type, + external_id_value: phone } body.append('Tags', JSON.stringify(tags)) From 6db1308b447b032c0ef8563753370620a20c0f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mar=C3=ADn=20Alcaraz?= Date: Wed, 29 Nov 2023 03:54:19 -0800 Subject: [PATCH 08/34] Add updateHandler to Actions DV360 (#1726) * Add updateHandler to Actions DV360 * Fix tests * update yarn.lock --- packages/cli/src/lib/server.ts | 1 + packages/destination-actions/package.json | 3 + .../display-video-360/__tests__/index.test.ts | 7 +- .../__tests__/shared.test.ts | 259 +++++++ .../addToAudience/generated-types.ts | 23 +- .../display-video-360/addToAudience/index.ts | 29 +- .../display-video-360/constants.ts | 5 +- .../destinations/display-video-360/errors.ts | 49 ++ .../display-video-360/generated-types.ts | 6 +- .../destinations/display-video-360/index.ts | 147 ++-- .../display-video-360/properties.ts | 54 ++ .../display-video-360/proto/protofile.ts | 662 ++++++++++++++++++ .../removeFromAudience/generated-types.ts | 23 +- .../removeFromAudience/index.ts | 27 +- .../destinations/display-video-360/shared.ts | 192 +++++ .../destinations/display-video-360/types.ts | 32 + yarn.lock | 76 ++ 17 files changed, 1508 insertions(+), 87 deletions(-) create mode 100644 packages/destination-actions/src/destinations/display-video-360/__tests__/shared.test.ts create mode 100644 packages/destination-actions/src/destinations/display-video-360/errors.ts create mode 100644 packages/destination-actions/src/destinations/display-video-360/properties.ts create mode 100644 packages/destination-actions/src/destinations/display-video-360/proto/protofile.ts create mode 100644 packages/destination-actions/src/destinations/display-video-360/shared.ts create mode 100644 packages/destination-actions/src/destinations/display-video-360/types.ts diff --git a/packages/cli/src/lib/server.ts b/packages/cli/src/lib/server.ts index 8b7be713b0..9d1f6c5333 100644 --- a/packages/cli/src/lib/server.ts +++ b/packages/cli/src/lib/server.ts @@ -301,6 +301,7 @@ function setupRoutes(def: DestinationDefinition | null): void { if (Array.isArray(eventParams.data)) { // If no mapping or default mapping is provided, default to using the first payload across all events. eventParams.mapping = mapping || eventParams.data[0] || {} + eventParams.audienceSettings = req.body.payload[0]?.context?.personas?.audience_settings || {} await action.executeBatch(eventParams) } else { await action.execute(eventParams) diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json index 3b0371a8b7..691775d92a 100644 --- a/packages/destination-actions/package.json +++ b/packages/destination-actions/package.json @@ -39,6 +39,9 @@ }, "dependencies": { "@amplitude/ua-parser-js": "^0.7.25", + "@bufbuild/buf": "^1.28.0", + "@bufbuild/protobuf": "^1.4.2", + "@bufbuild/protoc-gen-es": "^1.4.2", "@segment/a1-notation": "^2.1.4", "@segment/actions-core": "^3.88.0", "@segment/actions-shared": "^1.70.0", diff --git a/packages/destination-actions/src/destinations/display-video-360/__tests__/index.test.ts b/packages/destination-actions/src/destinations/display-video-360/__tests__/index.test.ts index ad6cf19e71..fff53c45c8 100644 --- a/packages/destination-actions/src/destinations/display-video-360/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/display-video-360/__tests__/index.test.ts @@ -9,19 +9,22 @@ const testDestination = createTestIntegration(Destination) const advertiserCreateAudienceUrl = CREATE_AUDIENCE_URL.replace('advertiserID', advertiserId) const advertiserGetAudienceUrl = GET_AUDIENCE_URL.replace('advertiserID', advertiserId) const expectedExternalID = `products/DISPLAY_VIDEO_ADVERTISER/customers/${advertiserId}/userLists/8457147615` +const accountType = 'DISPLAY_VIDEO_ADVERTISER' const createAudienceInput = { settings: {}, audienceName: '', audienceSettings: { - advertiserId: advertiserId + advertiserId: advertiserId, + accountType: accountType } } const getAudienceInput = { settings: {}, audienceSettings: { - advertiserId: advertiserId + advertiserId: advertiserId, + accountType: accountType }, audienceName: audienceName, externalId: expectedExternalID diff --git a/packages/destination-actions/src/destinations/display-video-360/__tests__/shared.test.ts b/packages/destination-actions/src/destinations/display-video-360/__tests__/shared.test.ts new file mode 100644 index 0000000000..f689f3eaa4 --- /dev/null +++ b/packages/destination-actions/src/destinations/display-video-360/__tests__/shared.test.ts @@ -0,0 +1,259 @@ +import nock from 'nock' +import { + buildHeaders, + assembleRawOps, + bulkUploaderResponseHandler, + createUpdateRequest, + sendUpdateRequest +} from '../shared' +import { AudienceSettings, Settings } from '../generated-types' +import { UpdateHandlerPayload } from '../types' +import { UpdateUsersDataResponse, ErrorCode, ErrorInfo } from '../proto/protofile' +import { StatsContext, Response } from '@segment/actions-core' +import createRequestClient from '../../../../../core/src/create-request-client' + +const oneMockPayload: UpdateHandlerPayload = { + external_audience_id: 'products/DISPLAY_VIDEO_ADVERTISER/customers/123/userLists/456', + google_gid: 'CAESEHIV8HXNp0pFdHgi2rElMfk', + mobile_advertising_id: '3b6e47b3-1437-4ba2-b3c9-446e4d0cd1e5', + anonymous_id: 'my-anon-id-42', + enable_batching: true +} + +const mockRequestClient = createRequestClient() + +const manyMockPayloads: UpdateHandlerPayload[] = [ + oneMockPayload, + { + external_audience_id: 'products/DISPLAY_VIDEO_ADVERTISER/customers/123/userLists/456', + anonymous_id: 'my-anon-id-43', + enable_batching: true + }, + { + external_audience_id: 'products/DISPLAY_VIDEO_ADVERTISER/customers/123/userLists/456', + google_gid: 'XNp0pFdHgi2rElMfk', + enable_batching: true + } +] + +const mockStatsClient = { + incr: jest.fn(), + observe: jest.fn(), + _name: jest.fn(), + _tags: jest.fn(), + histogram: jest.fn(), + set: jest.fn() +} + +const mockStatsContext = { + statsClient: mockStatsClient, + tags: [] +} as StatsContext + +const getRandomError = () => { + // possible errors for this stage are BAD_DATA, BAD_COOKIE, BAD_ATTRIBUTE_ID, BAD_NETWORK_ID. + const random = Math.floor(Math.random() * 4) + switch (random) { + case 0: + return ErrorCode.BAD_DATA + case 1: + return ErrorCode.BAD_COOKIE + case 2: + return ErrorCode.BAD_ATTRIBUTE_ID + case 3: + return ErrorCode.BAD_NETWORK_ID + } +} + +// Mock only the error code. The contents of the response are not important. +const createMockResponse = (errorCode: ErrorCode, payload: UpdateHandlerPayload[]) => { + const responseHandler = new UpdateUsersDataResponse() + responseHandler.status = errorCode + + if (errorCode === ErrorCode.PARTIAL_SUCCESS) { + // Making assumptions about IdType and UserId here because + // we are not currently testing their content therefore, it doesn't matter. + + const errors = payload.map((p) => { + const errorInfo = new ErrorInfo() + errorInfo.errorCode = getRandomError() + errorInfo.userListId = BigInt(p.external_audience_id.split('/').pop() || '-1') + errorInfo.userIdType = 0 + errorInfo.userId = p.google_gid || p.mobile_advertising_id || p.anonymous_id || '' + return errorInfo + }) + + responseHandler.errors = errors + } + + const b = Buffer.from(responseHandler.toBinary()) + const arrayBuffer = b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength) + + return new Response(arrayBuffer, { status: errorCode === ErrorCode.NO_ERROR ? 200 : 400 }) +} + +describe('shared', () => { + describe('buildHeaders', () => { + it('should build headers correctly', () => { + const audienceSettings: AudienceSettings = { + advertiserId: '123', + accountType: 'DISPLAY_VIDEO_ADVERTISER' + } + const settings: Settings = { + oauth: { + accessToken: 'real-token' + } + } + const result = buildHeaders(audienceSettings, settings) + expect(result).toEqual({ + Authorization: 'Bearer real-token', + 'Content-Type': 'application/json', + 'Login-Customer-Id': 'products/DISPLAY_VIDEO_ADVERTISER/customers/123' + }) + }) + }) + + describe('assembleRawOps', () => { + it('should return an array of UserOperation objects with IDFA', () => { + const results = assembleRawOps(oneMockPayload, 'add') + expect(results).toEqual([ + { + UserId: 'CAESEHIV8HXNp0pFdHgi2rElMfk', + UserIdType: 0, + UserListId: 456, + Delete: false + }, + { + UserId: '3b6e47b3-1437-4ba2-b3c9-446e4d0cd1e5', + UserIdType: 1, + UserListId: 456, + Delete: false + }, + { + UserId: 'my-anon-id-42', + UserIdType: 4, + UserListId: 456, + Delete: false + } + ]) + }) + + it('should return an array of UserOperation objects with Android Advertising ID', () => { + oneMockPayload.mobile_advertising_id = '3b6e47b314374ba2b3c9446e4d0cd1e5' + + const results = assembleRawOps(oneMockPayload, 'remove') + expect(results).toEqual([ + { + UserId: 'CAESEHIV8HXNp0pFdHgi2rElMfk', + UserIdType: 0, + UserListId: 456, + Delete: true + }, + { + UserId: '3b6e47b314374ba2b3c9446e4d0cd1e5', + UserIdType: 2, + UserListId: 456, + Delete: true + }, + { + UserId: 'my-anon-id-42', + UserIdType: 4, + UserListId: 456, + Delete: true + } + ]) + }) + }) + + // This method is used for both success and error cases. + // The easiest way to tell if something worked is to check the calls to statsClient + // The assumptions made around the payload are based on the error codes described in the protofile. + describe('bulkUploaderResponseHandler', () => { + it('handles success', async () => { + const mockResponse: Response = createMockResponse(ErrorCode.NO_ERROR, manyMockPayloads) + const statsName = 'addToAudience' + + await bulkUploaderResponseHandler(mockResponse, statsName, mockStatsContext) + expect(mockStatsClient.incr).toHaveBeenCalledWith(`${statsName}.success`, 1, mockStatsContext.tags) + }) + + it('handles 400 error', async () => { + const mockResponse: Response = createMockResponse(ErrorCode.BAD_COOKIE, manyMockPayloads) + const statsName = 'addToAudience' + + await bulkUploaderResponseHandler(mockResponse, statsName, mockStatsContext) + expect(mockStatsClient.incr).toHaveBeenCalledWith(`${statsName}.error.BAD_COOKIE`, 1, mockStatsContext.tags) + }) + + it('handles 500 error', async () => { + const mockResponse: Response = createMockResponse(ErrorCode.INTERNAL_ERROR, manyMockPayloads) + const statsName = 'removeFromAudience' + + await expect(bulkUploaderResponseHandler(mockResponse, statsName, mockStatsContext)).rejects.toThrow( + 'Bulk Uploader Internal Error' + ) + + expect(mockStatsClient.incr).toHaveBeenCalledWith(`${statsName}.error.INTERNAL_ERROR`, 1, mockStatsContext.tags) + }) + }) + + // If the request is invalid, its serialization will throw an error. + // No need to test the contents of the object because that is covered in assembleRawOps. + describe('createUpdateRequest', () => { + it('should create an UpdateUsersDataRequest object with the correct number of operations', () => { + const r = createUpdateRequest(manyMockPayloads, 'add') + expect(r.ops.length).toEqual(5) + }) + + it('should throw an error when unable to create UpdateUsersDataRequest', () => { + const mockPayload = { + enable_batching: true + } as UpdateHandlerPayload + expect(() => createUpdateRequest([mockPayload], 'remove')).toThrowError() + }) + }) + + // Not testing payload content here because it's covered by the bulkUploaderResponseHandler. + // Attempting to assemble a valid response payload is not worth the effort. + describe('sendUpdateRequest', () => { + it('should succeed', async () => { + nock('https://cm.g.doubleclick.net').post('/upload?nid=segment').reply(200) + + const r = createUpdateRequest(manyMockPayloads, 'add') + await sendUpdateRequest(mockRequestClient, r, 'addToAudience', mockStatsContext) + expect(mockStatsClient.incr).toHaveBeenCalledWith('addToAudience.success', 1, mockStatsContext.tags) + }) + + // To gracefully fails means that the request was successful, but some of the operations failed. + // The response will contain a list of errors. Its content is unknown. + // The endpoint will return a 400 status code. + it('should gracefully fail', async () => { + nock('https://cm.g.doubleclick.net').post('/upload?nid=segment').reply(400) + + UpdateUsersDataResponse.prototype.fromBinary = jest.fn(() => { + const responseHandler = new UpdateUsersDataResponse() + responseHandler.status = ErrorCode.PARTIAL_SUCCESS + responseHandler.errors = [ + { + errorCode: ErrorCode.BAD_DATA, + userListId: BigInt(456), + userIdType: 0, + userId: 'CAESEHIV8HXNp0pFdHgi2rElMfk' + } as ErrorInfo + ] + return responseHandler + }) + + const r = createUpdateRequest(manyMockPayloads, 'add') + await sendUpdateRequest(mockRequestClient, r, 'addToAudience', mockStatsContext) + expect(mockStatsClient.incr).toHaveBeenCalledWith('addToAudience.error.PARTIAL_SUCCESS', 1, mockStatsContext.tags) + }) + + it('should abruptly fail', async () => { + nock('https://cm.g.doubleclick.net').post('/upload?nid=segment').reply(500) + + const r = createUpdateRequest(manyMockPayloads, 'add') + await expect(sendUpdateRequest(mockRequestClient, r, 'addToAudience', mockStatsContext)).rejects.toThrow() + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/display-video-360/addToAudience/generated-types.ts b/packages/destination-actions/src/destinations/display-video-360/addToAudience/generated-types.ts index 944d22b085..cca92638f9 100644 --- a/packages/destination-actions/src/destinations/display-video-360/addToAudience/generated-types.ts +++ b/packages/destination-actions/src/destinations/display-video-360/addToAudience/generated-types.ts @@ -1,3 +1,24 @@ // Generated file. DO NOT MODIFY IT BY HAND. -export interface Payload {} +export interface Payload { + /** + * Enable batching of requests to the TikTok Audiences. + */ + enable_batching: boolean + /** + * The Audience ID in Google's DB. + */ + external_audience_id: string + /** + * Anonymous ID + */ + anonymous_id?: string + /** + * Mobile Advertising ID + */ + mobile_advertising_id?: string + /** + * Google GID + */ + google_gid?: string +} diff --git a/packages/destination-actions/src/destinations/display-video-360/addToAudience/index.ts b/packages/destination-actions/src/destinations/display-video-360/addToAudience/index.ts index ea890cabef..f87d2e02ca 100644 --- a/packages/destination-actions/src/destinations/display-video-360/addToAudience/index.ts +++ b/packages/destination-actions/src/destinations/display-video-360/addToAudience/index.ts @@ -1,13 +1,30 @@ import type { ActionDefinition } from '@segment/actions-core' -import type { Settings } from '../generated-types' + +import type { Settings, AudienceSettings } from '../generated-types' import type { Payload } from './generated-types' +import { handleUpdate } from '../shared' + +import { enable_batching, external_audience_id, google_gid, mobile_advertising_id, anonymous_id } from '../properties' -const action: ActionDefinition = { +const action: ActionDefinition = { title: 'Add to Audience', - description: 'Add users into an audience', - fields: {}, - perform: () => { - return + description: 'Add a user to a Display & Video 360 audience.', + fields: { + enable_batching: { ...enable_batching }, + external_audience_id: { ...external_audience_id }, + anonymous_id: { ...anonymous_id }, + mobile_advertising_id: { ...mobile_advertising_id }, + google_gid: { ...google_gid } + }, + perform: async (request, { payload, statsContext }) => { + statsContext?.statsClient?.incr('addToAudience', 1, statsContext?.tags) + await handleUpdate(request, [payload], 'add', statsContext) + return { success: true } + }, + performBatch: async (request, { payload, statsContext }) => { + statsContext?.statsClient?.incr('addToAudience.batch', 1, statsContext?.tags) + await handleUpdate(request, payload, 'add', statsContext) + return { success: true } } } diff --git a/packages/destination-actions/src/destinations/display-video-360/constants.ts b/packages/destination-actions/src/destinations/display-video-360/constants.ts index 78de1281a4..5c6acbeb00 100644 --- a/packages/destination-actions/src/destinations/display-video-360/constants.ts +++ b/packages/destination-actions/src/destinations/display-video-360/constants.ts @@ -1,4 +1,7 @@ export const GOOGLE_API_VERSION = 'v2' -export const BASE_URL = `https://audiencepartner.googleapis.com/${GOOGLE_API_VERSION}/products/DISPLAY_VIDEO_ADVERTISER/customers/advertiserID/` +// accountType and advertiserID are used as markers to be replaced in the code. DO NOT REMOVE THEM. +export const BASE_URL = `https://audiencepartner.googleapis.com/${GOOGLE_API_VERSION}/products/accountType/customers/advertiserID/` export const CREATE_AUDIENCE_URL = `${BASE_URL}userLists:mutate` export const GET_AUDIENCE_URL = `${BASE_URL}audiencePartner:searchStream` +export const OAUTH_URL = 'https://accounts.google.com/o/oauth2/token' +export const USER_UPLOAD_ENDPOINT = 'https://cm.g.doubleclick.net/upload?nid=segment' diff --git a/packages/destination-actions/src/destinations/display-video-360/errors.ts b/packages/destination-actions/src/destinations/display-video-360/errors.ts new file mode 100644 index 0000000000..082949cfe1 --- /dev/null +++ b/packages/destination-actions/src/destinations/display-video-360/errors.ts @@ -0,0 +1,49 @@ +import { ErrorCodes, IntegrationError } from '@segment/actions-core' +import { InvalidAuthenticationError } from '@segment/actions-core/*' + +import { GoogleAPIError } from './types' + +const isGoogleAPIError = (error: unknown): error is GoogleAPIError => { + if (typeof error === 'object' && error !== null) { + const e = error as GoogleAPIError + // Not using any forces us to check for all the properties we need. + return ( + typeof e.response === 'object' && + e.response !== null && + typeof e.response.status === 'number' && + typeof e.response.data === 'object' && + e.response.data !== null && + typeof e.response.data.error === 'object' && + e.response.data.error !== null && + typeof e.response.data.error.message === 'string' + ) + } + return false +} + +// This method follows the retry logic defined in IntegrationError in the actions-core package +export const handleRequestError = (error: unknown) => { + if (!isGoogleAPIError(error)) { + if (!error) { + return new IntegrationError('Unknown error', 'UNKNOWN_ERROR', 500) + } + } + + const gError = error as GoogleAPIError + const code = gError.response?.status + const message = gError.response?.data?.error?.message + + if (code === 401) { + return new InvalidAuthenticationError(message, ErrorCodes.INVALID_AUTHENTICATION) + } + + if (code === 501) { + return new IntegrationError(message, 'INTEGRATION_ERROR', 501) + } + + if (code === 408 || code === 423 || code === 429 || code >= 500) { + return new IntegrationError(message, 'RETRYABLE_ERROR', code) + } + + return new IntegrationError(message, 'INTEGRATION_ERROR', code) +} diff --git a/packages/destination-actions/src/destinations/display-video-360/generated-types.ts b/packages/destination-actions/src/destinations/display-video-360/generated-types.ts index 99e24e4b6b..b36920d339 100644 --- a/packages/destination-actions/src/destinations/display-video-360/generated-types.ts +++ b/packages/destination-actions/src/destinations/display-video-360/generated-types.ts @@ -7,5 +7,9 @@ export interface AudienceSettings { /** * The ID of your advertiser, used throughout Display & Video 360. Use this ID when you contact Display & Video 360 support to help our teams locate your specific account. */ - advertiserId?: string + advertiserId: string + /** + * The type of the advertiser account you have linked to this Display & Video 360 destination. + */ + accountType: string } diff --git a/packages/destination-actions/src/destinations/display-video-360/index.ts b/packages/destination-actions/src/destinations/display-video-360/index.ts index b19a7220f8..7cb04d4c0b 100644 --- a/packages/destination-actions/src/destinations/display-video-360/index.ts +++ b/packages/destination-actions/src/destinations/display-video-360/index.ts @@ -1,14 +1,14 @@ import { AudienceDestinationDefinition, IntegrationError } from '@segment/actions-core' + import type { Settings, AudienceSettings } from './generated-types' +import type { RefreshTokenResponse } from './types' import addToAudience from './addToAudience' import removeFromAudience from './removeFromAudience' -import { CREATE_AUDIENCE_URL, GET_AUDIENCE_URL } from './constants' - -interface RefreshTokenResponse { - access_token: string -} +import { CREATE_AUDIENCE_URL, GET_AUDIENCE_URL, OAUTH_URL } from './constants' +import { buildHeaders } from './shared' +import { handleRequestError } from './errors' const destination: AudienceDestinationDefinition = { name: 'Display and Video 360 (Actions)', @@ -16,11 +16,9 @@ const destination: AudienceDestinationDefinition = { mode: 'cloud', authentication: { scheme: 'oauth2', - fields: { - //Fields is required, so this is left empty - }, + fields: {}, // Fields is required. Left empty on purpose. refreshAccessToken: async (request, { auth }) => { - const { data } = await request('https://accounts.google.com/o/oauth2/token', { + const { data } = await request(OAUTH_URL, { method: 'POST', body: new URLSearchParams({ refresh_token: auth.refreshToken, @@ -32,20 +30,24 @@ const destination: AudienceDestinationDefinition = { return { accessToken: data.access_token } } }, - extendRequest({ auth }) { - // TODO: extendRequest doesn't work within createAudience and getAudience - return { - headers: { - authorization: `Bearer ${auth?.accessToken}` - } - } - }, audienceFields: { advertiserId: { type: 'string', label: 'Advertiser ID', + required: true, description: 'The ID of your advertiser, used throughout Display & Video 360. Use this ID when you contact Display & Video 360 support to help our teams locate your specific account.' + }, + accountType: { + type: 'string', + label: 'Account Type', + description: 'The type of the advertiser account you have linked to this Display & Video 360 destination.', + required: true, + choices: [ + { label: 'DISPLAY_VIDEO_ADVERTISER', value: 'DISPLAY_VIDEO_ADVERTISER' }, + { label: 'DISPLAY_VIDEO_PARTNER', value: 'DISPLAY_VIDEO_PARTNER' }, + { label: 'DFP_BY_GOOGLE or GOOGLE_AD_MANAGER', value: 'GOOGLE_AD_MANAGER' } + ] } }, audienceConfig: { @@ -54,10 +56,9 @@ const destination: AudienceDestinationDefinition = { full_audience_sync: true }, async createAudience(request, createAudienceInput) { - const audienceName = createAudienceInput.audienceName - const advertiserId = createAudienceInput.audienceSettings?.advertiserId - const statsClient = createAudienceInput?.statsContext?.statsClient - const statsTags = createAudienceInput?.statsContext?.tags + const { audienceName, audienceSettings, statsContext, settings } = createAudienceInput + const { advertiserId, accountType } = audienceSettings || {} + const { statsClient, tags: statsTags } = statsContext || {} if (!audienceName) { throw new IntegrationError('Missing audience name value', 'MISSING_REQUIRED_FIELD', 400) @@ -67,86 +68,92 @@ const destination: AudienceDestinationDefinition = { throw new IntegrationError('Missing advertiser ID value', 'MISSING_REQUIRED_FIELD', 400) } - const partnerCreateAudienceUrl = CREATE_AUDIENCE_URL.replace('advertiserID', advertiserId) + if (!accountType) { + throw new IntegrationError('Missing account type value', 'MISSING_REQUIRED_FIELD', 400) + } + + const listTypeMap = { basicUserList: {}, type: 'REMARKETING', membershipStatus: 'OPEN' } + const partnerCreateAudienceUrl = CREATE_AUDIENCE_URL.replace('advertiserID', advertiserId).replace( + 'accountType', + accountType + ) + let response try { response = await request(partnerCreateAudienceUrl, { + headers: buildHeaders(createAudienceInput.audienceSettings, settings), method: 'POST', - headers: { - // 'Authorization': `Bearer ${authToken}`, // TODO: Replace with auth token - 'Content-Type': 'application/json', - 'Login-Customer-Id': `products/DISPLAY_VIDEO_ADVERTISER/customers/${advertiserId}` - }, json: { operations: [ { create: { - basicUserList: {}, + ...listTypeMap, name: audienceName, description: 'Created by Segment', - membershipStatus: 'OPEN', - type: 'REMARKETING', membershipLifeSpan: '540' } } ] } }) - } catch (error) { - const errorMessage = await JSON.parse(error.response.content).error.details[0].errors[0].message - statsClient?.incr('createAudience.error', 1, statsTags) - throw new IntegrationError(errorMessage, 'INVALID_RESPONSE', 400) - } - const r = await response.json() - statsClient?.incr('createAudience.success', 1, statsTags) + const r = await response?.json() + statsClient?.incr('createAudience.success', 1, statsTags) - return { - externalId: r['results'][0]['resourceName'] + return { + externalId: r['results'][0]['resourceName'] + } + } catch (error) { + statsClient?.incr('createAudience.error', 1, statsTags) + throw handleRequestError(error) } }, async getAudience(request, getAudienceInput) { - const statsClient = getAudienceInput?.statsContext?.statsClient - const statsTags = getAudienceInput?.statsContext?.tags - const advertiserId = getAudienceInput.audienceSettings?.advertiserId + const { statsContext, audienceSettings, settings } = getAudienceInput + const { statsClient, tags: statsTags } = statsContext || {} + const { advertiserId, accountType } = audienceSettings || {} if (!advertiserId) { throw new IntegrationError('Missing required advertiser ID value', 'MISSING_REQUIRED_FIELD', 400) } - const advertiserGetAudienceUrl = GET_AUDIENCE_URL.replace('advertiserID', advertiserId) - const response = await request(advertiserGetAudienceUrl, { - headers: { - // 'Authorization': `Bearer ${authToken}`, // TODO: Replace with auth token - 'Content-Type': 'application/json', - 'Login-Customer-Id': `products/DISPLAY_VIDEO_ADVERTISER/customers/${advertiserId}` - }, - method: 'POST', - json: { - query: `SELECT user_list.name, user_list.description, user_list.membership_status, user_list.match_rate_percentage FROM user_list WHERE user_list.resource_name = "${getAudienceInput.externalId}"` - } - }) + if (!accountType) { + throw new IntegrationError('Missing account type value', 'MISSING_REQUIRED_FIELD', 400) + } - const r = await response.json() + const advertiserGetAudienceUrl = GET_AUDIENCE_URL.replace('advertiserID', advertiserId).replace( + 'accountType', + accountType + ) - if (response.status !== 200) { - statsClient?.incr('getAudience.error', 1, statsTags) - throw new IntegrationError('Invalid response from get audience request', 'INVALID_RESPONSE', 400) - } + try { + const response = await request(advertiserGetAudienceUrl, { + headers: buildHeaders(audienceSettings, settings), + method: 'POST', + json: { + query: `SELECT user_list.name, user_list.description, user_list.membership_status, user_list.match_rate_percentage FROM user_list WHERE user_list.resource_name = "${getAudienceInput.externalId}"` + } + }) - const externalId = r[0]?.results[0]?.userList?.resourceName + const r = await response.json() - if (externalId !== getAudienceInput.externalId) { - throw new IntegrationError( - "Unable to verify ownership over audience. Segment Audience ID doesn't match Googles Audience ID.", - 'INVALID_REQUEST_DATA', - 400 - ) - } + const externalId = r[0]?.results[0]?.userList?.resourceName + + if (externalId !== getAudienceInput.externalId) { + throw new IntegrationError( + "Unable to verify ownership over audience. Segment Audience ID doesn't match Googles Audience ID.", + 'INVALID_REQUEST_DATA', + 400 + ) + } - statsClient?.incr('getAudience.success', 1, statsTags) - return { - externalId: externalId + statsClient?.incr('getAudience.success', 1, statsTags) + return { + externalId: externalId + } + } catch (error) { + statsClient?.incr('getAudience.error', 1, statsTags) + throw handleRequestError(error) } } }, diff --git a/packages/destination-actions/src/destinations/display-video-360/properties.ts b/packages/destination-actions/src/destinations/display-video-360/properties.ts new file mode 100644 index 0000000000..c621789e3d --- /dev/null +++ b/packages/destination-actions/src/destinations/display-video-360/properties.ts @@ -0,0 +1,54 @@ +import { InputField } from '@segment/actions-core/destination-kit/types' + +export const anonymous_id: InputField = { + label: 'Anonymous ID', + description: 'Anonymous ID', + type: 'string', + required: false, + default: { + '@path': '$.anonymousId' + }, + readOnly: true +} + +export const mobile_advertising_id: InputField = { + label: 'Mobile Advertising ID', + description: 'Mobile Advertising ID', + type: 'string', + required: false, + default: { + '@path': '$.context.device.advertisingId' + }, + readOnly: true +} + +export const google_gid: InputField = { + label: 'Google GID', + description: 'Google GID', + type: 'string', + required: false, + default: { + '@path': '$.context.traits.google_gid' // TODO: Double check on this one because it might need to be explicitly set. + }, + readOnly: true +} + +export const enable_batching: InputField = { + label: 'Enable Batching', + description: 'Enable batching of requests to the TikTok Audiences.', + type: 'boolean', + default: true, + required: true, + unsafe_hidden: true +} + +export const external_audience_id: InputField = { + label: 'External Audience ID', + description: "The Audience ID in Google's DB.", + type: 'string', + required: true, + unsafe_hidden: true, + default: { + '@path': '$.context.personas.external_audience_id' + } +} diff --git a/packages/destination-actions/src/destinations/display-video-360/proto/protofile.ts b/packages/destination-actions/src/destinations/display-video-360/proto/protofile.ts new file mode 100644 index 0000000000..bcb8848f78 --- /dev/null +++ b/packages/destination-actions/src/destinations/display-video-360/proto/protofile.ts @@ -0,0 +1,662 @@ +// @generated by protoc-gen-es v1.2.0 with parameter "target=ts" +// @generated from file dmp.proto (syntax proto2) +/* eslint-disable */ +// @ts-nocheck + +import type { + BinaryReadOptions, + FieldList, + JsonReadOptions, + JsonValue, + PartialMessage, + PlainMessage +} from '@bufbuild/protobuf' +import { Message, proto2, protoInt64 } from '@bufbuild/protobuf' + +/** + * The type of identifier being uploaded. + * + * @generated from enum UserIdType + */ +export enum UserIdType { + /** + * A user identifier received through the cookie matching service. + * + * @generated from enum value: GOOGLE_USER_ID = 0; + */ + GOOGLE_USER_ID = 0, + + /** + * iOS Advertising ID. + * + * @generated from enum value: IDFA = 1; + */ + IDFA = 1, + + /** + * Android Advertising ID. + * + * @generated from enum value: ANDROID_ADVERTISING_ID = 2; + */ + ANDROID_ADVERTISING_ID = 2, + + /** + * Roku ID. + * + * @generated from enum value: RIDA = 5; + */ + RIDA = 5, + + /** + * Amazon Fire TV ID. + * + * @generated from enum value: AFAI = 6; + */ + AFAI = 6, + + /** + * XBOX/Microsoft ID. + * + * @generated from enum value: MSAI = 7; + */ + MSAI = 7, + + /** + * A "generic" category for any UUID formatted device provided ID. + * Allows partner uploads without needing to select a specific, + * pre-existing Device ID type. + * + * @generated from enum value: GENERIC_DEVICE_ID = 9; + */ + GENERIC_DEVICE_ID = 9, + + /** + * Partner provided ID. User identifier in partner's namespace. + * If the partner has sent the partner user identifier during cookie matching, + * then Google will be able to store user list membership associated with + * the partner's user identifier. + * See cookie matching documentation: + * https://developers.google.com/authorized-buyers/rtb/cookie-guide + * + * @generated from enum value: PARTNER_PROVIDED_ID = 4; + */ + PARTNER_PROVIDED_ID = 4 +} +// Retrieve enum metadata with: proto2.getEnumType(UserIdType) +proto2.util.setEnumType(UserIdType, 'UserIdType', [ + { no: 0, name: 'GOOGLE_USER_ID' }, + { no: 1, name: 'IDFA' }, + { no: 2, name: 'ANDROID_ADVERTISING_ID' }, + { no: 5, name: 'RIDA' }, + { no: 6, name: 'AFAI' }, + { no: 7, name: 'MSAI' }, + { no: 9, name: 'GENERIC_DEVICE_ID' }, + { no: 4, name: 'PARTNER_PROVIDED_ID' } +]) + +/** + * Notification code. + * + * @generated from enum NotificationCode + */ +export enum NotificationCode { + /** + * A cookie is considered inactive if Google has not seen any activity related + * to the cookie in several days. + * + * @generated from enum value: INACTIVE_COOKIE = 0; + */ + INACTIVE_COOKIE = 0 +} +// Retrieve enum metadata with: proto2.getEnumType(NotificationCode) +proto2.util.setEnumType(NotificationCode, 'NotificationCode', [{ no: 0, name: 'INACTIVE_COOKIE' }]) + +/** + * Notification status code. + * + * @generated from enum NotificationStatus + */ +export enum NotificationStatus { + /** + * No need to send notifications for this request. + * + * @generated from enum value: NO_NOTIFICATION = 0; + */ + NO_NOTIFICATION = 0, + + /** + * Google decided to not send notifications, even though there were + * notifications to send. + * + * @generated from enum value: NOTIFICATIONS_OMITTED = 1; + */ + NOTIFICATIONS_OMITTED = 1 +} +// Retrieve enum metadata with: proto2.getEnumType(NotificationStatus) +proto2.util.setEnumType(NotificationStatus, 'NotificationStatus', [ + { no: 0, name: 'NO_NOTIFICATION' }, + { no: 1, name: 'NOTIFICATIONS_OMITTED' } +]) + +/** + * Response error codes. + * + * @generated from enum ErrorCode + */ +export enum ErrorCode { + /** + * @generated from enum value: NO_ERROR = 0; + */ + NO_ERROR = 0, + + /** + * Some of the user data operations failed. See comments in the + * UpdateUserDataResponse + * + * @generated from enum value: PARTIAL_SUCCESS = 1; + */ + PARTIAL_SUCCESS = 1, + + /** + * Provided network_id cannot add data to attribute_id or non-HTTPS. + * + * @generated from enum value: PERMISSION_DENIED = 2; + */ + PERMISSION_DENIED = 2, + + /** + * Cannot parse payload. + * + * @generated from enum value: BAD_DATA = 3; + */ + BAD_DATA = 3, + + /** + * Cannot decode provided cookie. + * + * @generated from enum value: BAD_COOKIE = 4; + */ + BAD_COOKIE = 4, + + /** + * Invalid or closed user_list_id. + * + * @generated from enum value: BAD_ATTRIBUTE_ID = 5; + */ + BAD_ATTRIBUTE_ID = 5, + + /** + * An invalid nid parameter was provided in the request. + * + * @generated from enum value: BAD_NETWORK_ID = 7; + */ + BAD_NETWORK_ID = 7, + + /** + * Request payload size over allowed limit. + * + * @generated from enum value: REQUEST_TOO_BIG = 8; + */ + REQUEST_TOO_BIG = 8, + + /** + * No UserDataOperation messages in UpdateUsersDataRequest. + * + * @generated from enum value: EMPTY_REQUEST = 9; + */ + EMPTY_REQUEST = 9, + + /** + * The server could not process the request due to an internal error. Retrying + * the same request later is suggested. + * + * @generated from enum value: INTERNAL_ERROR = 10; + */ + INTERNAL_ERROR = 10, + + /** + * Bad data_source_id -- most likely out of range from [1, 1000]. + * + * @generated from enum value: BAD_DATA_SOURCE_ID = 11; + */ + BAD_DATA_SOURCE_ID = 11, + + /** + * The timestamp is a past/future time that is too far from current time. + * + * @generated from enum value: BAD_TIMESTAMP = 12; + */ + BAD_TIMESTAMP = 12, + + /** + * Missing internal mapping. + * If operation is PARTNER_PROVIDED_ID, then this error means our mapping + * table does not contain corresponding google user id. This mapping is + * recorded during Cookie Matching. + * For other operations, then it may be internal error. + * + * @generated from enum value: UNKNOWN_ID = 21; + */ + UNKNOWN_ID = 21 +} +// Retrieve enum metadata with: proto2.getEnumType(ErrorCode) +proto2.util.setEnumType(ErrorCode, 'ErrorCode', [ + { no: 0, name: 'NO_ERROR' }, + { no: 1, name: 'PARTIAL_SUCCESS' }, + { no: 2, name: 'PERMISSION_DENIED' }, + { no: 3, name: 'BAD_DATA' }, + { no: 4, name: 'BAD_COOKIE' }, + { no: 5, name: 'BAD_ATTRIBUTE_ID' }, + { no: 7, name: 'BAD_NETWORK_ID' }, + { no: 8, name: 'REQUEST_TOO_BIG' }, + { no: 9, name: 'EMPTY_REQUEST' }, + { no: 10, name: 'INTERNAL_ERROR' }, + { no: 11, name: 'BAD_DATA_SOURCE_ID' }, + { no: 12, name: 'BAD_TIMESTAMP' }, + { no: 21, name: 'UNKNOWN_ID' } +]) + +/** + * Update data for a single user. + * + * @generated from message UserDataOperation + */ +export class UserDataOperation extends Message { + /** + * User id. The type is determined by the user_id_type field. + * + * Must always be present. Specifies which user this operation applies to. + * + * @generated from field: optional string user_id = 1 [default = ""]; + */ + userId?: string + + /** + * The type of the user id. + * + * @generated from field: optional UserIdType user_id_type = 14 [default = GOOGLE_USER_ID]; + */ + userIdType?: UserIdType + + /** + * The id of the userlist. This can be retrieved from the AdX UI for AdX + * customers, the AdWords API for non-AdX customers, or through your Technical + * Account Manager. + * + * @generated from field: optional int64 user_list_id = 4 [default = 0]; + */ + userListId?: bigint + + /** + * Optional time (seconds since the epoch) when the user performed an action + * causing them to be added to the list. Using the default value of 0 + * indicates that the current time on the server should be used. + * + * @generated from field: optional int64 time_added_to_user_list = 5 [default = 0]; + */ + timeAddedToUserList?: bigint + + /** + * Same as time_added_to_user_list but with finer grained time resolution, in + * microseconds. If both timestamps are specified, + * time_added_to_user_list_in_usec will be used. + * + * @generated from field: optional int64 time_added_to_user_list_in_usec = 8 [default = 0]; + */ + timeAddedToUserListInUsec?: bigint + + /** + * Set to true if the operation is a deletion. + * + * @generated from field: optional bool delete = 6 [default = false]; + */ + delete?: boolean + + /** + * Set true if the user opted out from being targeted. + * + * @generated from field: optional bool opt_out = 12 [default = false]; + */ + optOut?: boolean + + /** + * An id indicating the data source which contributed this membership. The id + * is required to be in the range of 1 to 1000 and any ids greater than this + * will result in an error of type BAD_DATA_SOURCE_ID. These ids don't have + * any semantics for Google and may be used as labels for reporting purposes. + * + * @generated from field: optional int32 data_source_id = 7 [default = 0]; + */ + dataSourceId?: number + + constructor(data?: PartialMessage) { + super() + proto2.util.initPartial(data, this) + } + + static readonly runtime: typeof proto2 = proto2 + static readonly typeName = 'UserDataOperation' + static readonly fields: FieldList = proto2.util.newFieldList(() => [ + { no: 1, name: 'user_id', kind: 'scalar', T: 9 /* ScalarType.STRING */, opt: true, default: '' }, + { + no: 14, + name: 'user_id_type', + kind: 'enum', + T: proto2.getEnumType(UserIdType), + opt: true, + default: UserIdType.GOOGLE_USER_ID + }, + { + no: 4, + name: 'user_list_id', + kind: 'scalar', + T: 3 /* ScalarType.INT64 */, + opt: true, + default: protoInt64.parse('0') + }, + { + no: 5, + name: 'time_added_to_user_list', + kind: 'scalar', + T: 3 /* ScalarType.INT64 */, + opt: true, + default: protoInt64.parse('0') + }, + { + no: 8, + name: 'time_added_to_user_list_in_usec', + kind: 'scalar', + T: 3 /* ScalarType.INT64 */, + opt: true, + default: protoInt64.parse('0') + }, + { no: 6, name: 'delete', kind: 'scalar', T: 8 /* ScalarType.BOOL */, opt: true, default: false }, + { no: 12, name: 'opt_out', kind: 'scalar', T: 8 /* ScalarType.BOOL */, opt: true, default: false }, + { no: 7, name: 'data_source_id', kind: 'scalar', T: 5 /* ScalarType.INT32 */, opt: true, default: 0 } + ]) + + static fromBinary(bytes: Uint8Array, options?: Partial): UserDataOperation { + return new UserDataOperation().fromBinary(bytes, options) + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UserDataOperation { + return new UserDataOperation().fromJson(jsonValue, options) + } + + static fromJsonString(jsonString: string, options?: Partial): UserDataOperation { + return new UserDataOperation().fromJsonString(jsonString, options) + } + + static equals( + a: UserDataOperation | PlainMessage | undefined, + b: UserDataOperation | PlainMessage | undefined + ): boolean { + return proto2.util.equals(UserDataOperation, a, b) + } +} + +/** + * This protocol buffer is used to update user data. It is sent as the payload + * of an HTTPS POST request with the Content-Type header set to + * "application/octet-stream" (preferrably Content-Encoding: gzip). + * + * @generated from message UpdateUsersDataRequest + */ +export class UpdateUsersDataRequest extends Message { + /** + * Multiple operations over user attributes or user lists. + * + * @generated from field: repeated UserDataOperation ops = 1; + */ + ops: UserDataOperation[] = [] + + /** + * If true, request sending notifications about the given users in the + * response. Note that in some circumstances notifications may not be sent + * even if requested. In this case the notification_status field of the + * response will be set to NOTIFICATIONS_OMITTED. + * + * @generated from field: optional bool send_notifications = 2 [default = false]; + */ + sendNotifications?: boolean + + constructor(data?: PartialMessage) { + super() + proto2.util.initPartial(data, this) + } + + static readonly runtime: typeof proto2 = proto2 + static readonly typeName = 'UpdateUsersDataRequest' + static readonly fields: FieldList = proto2.util.newFieldList(() => [ + { no: 1, name: 'ops', kind: 'message', T: UserDataOperation, repeated: true }, + { no: 2, name: 'send_notifications', kind: 'scalar', T: 8 /* ScalarType.BOOL */, opt: true, default: false } + ]) + + static fromBinary(bytes: Uint8Array, options?: Partial): UpdateUsersDataRequest { + return new UpdateUsersDataRequest().fromBinary(bytes, options) + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UpdateUsersDataRequest { + return new UpdateUsersDataRequest().fromJson(jsonValue, options) + } + + static fromJsonString(jsonString: string, options?: Partial): UpdateUsersDataRequest { + return new UpdateUsersDataRequest().fromJsonString(jsonString, options) + } + + static equals( + a: UpdateUsersDataRequest | PlainMessage | undefined, + b: UpdateUsersDataRequest | PlainMessage | undefined + ): boolean { + return proto2.util.equals(UpdateUsersDataRequest, a, b) + } +} + +/** + * Information about an error with an individual user operation. + * + * @generated from message ErrorInfo + */ +export class ErrorInfo extends Message { + /** + * The user_list_id in the request which caused problems. This may be empty + * if the problem was with a particular user id. + * + * @generated from field: optional int64 user_list_id = 2 [default = 0]; + */ + userListId?: bigint + + /** + * The user_id which caused problems. This may be empty if other data was bad + * regardless of a cookie. + * + * @generated from field: optional string user_id = 3 [default = ""]; + */ + userId?: string + + /** + * The type of the user ID. + * + * @generated from field: optional UserIdType user_id_type = 7 [default = GOOGLE_USER_ID]; + */ + userIdType?: UserIdType + + /** + * @generated from field: optional ErrorCode error_code = 4; + */ + errorCode?: ErrorCode + + constructor(data?: PartialMessage) { + super() + proto2.util.initPartial(data, this) + } + + static readonly runtime: typeof proto2 = proto2 + static readonly typeName = 'ErrorInfo' + static readonly fields: FieldList = proto2.util.newFieldList(() => [ + { + no: 2, + name: 'user_list_id', + kind: 'scalar', + T: 3 /* ScalarType.INT64 */, + opt: true, + default: protoInt64.parse('0') + }, + { no: 3, name: 'user_id', kind: 'scalar', T: 9 /* ScalarType.STRING */, opt: true, default: '' }, + { + no: 7, + name: 'user_id_type', + kind: 'enum', + T: proto2.getEnumType(UserIdType), + opt: true, + default: UserIdType.GOOGLE_USER_ID + }, + { no: 4, name: 'error_code', kind: 'enum', T: proto2.getEnumType(ErrorCode), opt: true } + ]) + + static fromBinary(bytes: Uint8Array, options?: Partial): ErrorInfo { + return new ErrorInfo().fromBinary(bytes, options) + } + + static fromJson(jsonValue: JsonValue, options?: Partial): ErrorInfo { + return new ErrorInfo().fromJson(jsonValue, options) + } + + static fromJsonString(jsonString: string, options?: Partial): ErrorInfo { + return new ErrorInfo().fromJsonString(jsonString, options) + } + + static equals( + a: ErrorInfo | PlainMessage | undefined, + b: ErrorInfo | PlainMessage | undefined + ): boolean { + return proto2.util.equals(ErrorInfo, a, b) + } +} + +/** + * Per user notification information. + * + * @generated from message NotificationInfo + */ +export class NotificationInfo extends Message { + /** + * The user_id for which the notification applies. One of the user_ids sent + * in a UserDataOperation. + * + * @generated from field: optional string user_id = 1 [default = ""]; + */ + userId?: string + + /** + * @generated from field: optional NotificationCode notification_code = 2; + */ + notificationCode?: NotificationCode + + constructor(data?: PartialMessage) { + super() + proto2.util.initPartial(data, this) + } + + static readonly runtime: typeof proto2 = proto2 + static readonly typeName = 'NotificationInfo' + static readonly fields: FieldList = proto2.util.newFieldList(() => [ + { no: 1, name: 'user_id', kind: 'scalar', T: 9 /* ScalarType.STRING */, opt: true, default: '' }, + { no: 2, name: 'notification_code', kind: 'enum', T: proto2.getEnumType(NotificationCode), opt: true } + ]) + + static fromBinary(bytes: Uint8Array, options?: Partial): NotificationInfo { + return new NotificationInfo().fromBinary(bytes, options) + } + + static fromJson(jsonValue: JsonValue, options?: Partial): NotificationInfo { + return new NotificationInfo().fromJson(jsonValue, options) + } + + static fromJsonString(jsonString: string, options?: Partial): NotificationInfo { + return new NotificationInfo().fromJsonString(jsonString, options) + } + + static equals( + a: NotificationInfo | PlainMessage | undefined, + b: NotificationInfo | PlainMessage | undefined + ): boolean { + return proto2.util.equals(NotificationInfo, a, b) + } +} + +/** + * Response to the UpdateUsersDataRequest. Sent in HTTP response to the + * original POST request, with the Content-Type header set to + * "application/octet-stream". The HTTP response status is either 200 (no + * errors) or 400, in which case the protocol buffer will provide error details. + * + * @generated from message UpdateUsersDataResponse + */ +export class UpdateUsersDataResponse extends Message { + /** + * When status == PARTIAL_SUCCESS, some (not all) of the operations failed and + * the "errors" field has details on the types and number of errors + * encountered. When status == NO_ERROR, all the data was imported + * successfully. When status > PARTIAL_SUCCESS no data was imported. + * + * @generated from field: optional ErrorCode status = 1; + */ + status?: ErrorCode + + /** + * Each operation that failed is reported as a separate error here when + * status == PARTIAL_SUCCESS. + * + * @generated from field: repeated ErrorInfo errors = 2; + */ + errors: ErrorInfo[] = [] + + /** + * Useful, non-error, information about the user ids in the request. Each + * NotificationInfo provides information about a single user id. Only sent if send_notifications is set to true. + * + * @generated from field: repeated NotificationInfo notifications = 3; + */ + notifications: NotificationInfo[] = [] + + /** + * Indicates why a notification has not been sent. + * + * @generated from field: optional NotificationStatus notification_status = 4; + */ + notificationStatus?: NotificationStatus + + constructor(data?: PartialMessage) { + super() + proto2.util.initPartial(data, this) + } + + static readonly runtime: typeof proto2 = proto2 + static readonly typeName = 'UpdateUsersDataResponse' + static readonly fields: FieldList = proto2.util.newFieldList(() => [ + { no: 1, name: 'status', kind: 'enum', T: proto2.getEnumType(ErrorCode), opt: true }, + { no: 2, name: 'errors', kind: 'message', T: ErrorInfo, repeated: true }, + { no: 3, name: 'notifications', kind: 'message', T: NotificationInfo, repeated: true }, + { no: 4, name: 'notification_status', kind: 'enum', T: proto2.getEnumType(NotificationStatus), opt: true } + ]) + + static fromBinary(bytes: Uint8Array, options?: Partial): UpdateUsersDataResponse { + return new UpdateUsersDataResponse().fromBinary(bytes, options) + } + + static fromJson(jsonValue: JsonValue, options?: Partial): UpdateUsersDataResponse { + return new UpdateUsersDataResponse().fromJson(jsonValue, options) + } + + static fromJsonString(jsonString: string, options?: Partial): UpdateUsersDataResponse { + return new UpdateUsersDataResponse().fromJsonString(jsonString, options) + } + + static equals( + a: UpdateUsersDataResponse | PlainMessage | undefined, + b: UpdateUsersDataResponse | PlainMessage | undefined + ): boolean { + return proto2.util.equals(UpdateUsersDataResponse, a, b) + } +} diff --git a/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/generated-types.ts b/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/generated-types.ts index 944d22b085..cca92638f9 100644 --- a/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/generated-types.ts +++ b/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/generated-types.ts @@ -1,3 +1,24 @@ // Generated file. DO NOT MODIFY IT BY HAND. -export interface Payload {} +export interface Payload { + /** + * Enable batching of requests to the TikTok Audiences. + */ + enable_batching: boolean + /** + * The Audience ID in Google's DB. + */ + external_audience_id: string + /** + * Anonymous ID + */ + anonymous_id?: string + /** + * Mobile Advertising ID + */ + mobile_advertising_id?: string + /** + * Google GID + */ + google_gid?: string +} diff --git a/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/index.ts b/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/index.ts index f1baa478e0..a819f46419 100644 --- a/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/index.ts +++ b/packages/destination-actions/src/destinations/display-video-360/removeFromAudience/index.ts @@ -1,13 +1,30 @@ import type { ActionDefinition } from '@segment/actions-core' -import type { Settings } from '../generated-types' + +import type { Settings, AudienceSettings } from '../generated-types' import type { Payload } from './generated-types' +import { handleUpdate } from '../shared' + +import { enable_batching, external_audience_id, anonymous_id, mobile_advertising_id, google_gid } from '../properties' -const action: ActionDefinition = { +const action: ActionDefinition = { title: 'Remove from Audience', description: 'Remove users from an audience', - fields: {}, - perform: () => { - return + fields: { + enable_batching: { ...enable_batching }, + external_audience_id: { ...external_audience_id }, + anonymous_id: { ...anonymous_id }, + mobile_advertising_id: { ...mobile_advertising_id }, + google_gid: { ...google_gid } + }, + perform: async (request, { payload, statsContext }) => { + statsContext?.statsClient?.incr('removeFromAudience', 1, statsContext?.tags) + await handleUpdate(request, [payload], 'remove', statsContext) + return { success: true } + }, + performBatch: async (request, { payload, statsContext }) => { + statsContext?.statsClient?.incr('removeFromAudience.batch', 1, statsContext?.tags) + await handleUpdate(request, payload, 'remove', statsContext) + return { success: true } } } diff --git a/packages/destination-actions/src/destinations/display-video-360/shared.ts b/packages/destination-actions/src/destinations/display-video-360/shared.ts new file mode 100644 index 0000000000..3de767f76c --- /dev/null +++ b/packages/destination-actions/src/destinations/display-video-360/shared.ts @@ -0,0 +1,192 @@ +import { IntegrationError, RequestClient, StatsContext } from '@segment/actions-core' +import { USER_UPLOAD_ENDPOINT } from './constants' + +import { + UserIdType, + UpdateUsersDataRequest, + UserDataOperation, + UpdateUsersDataResponse, + ErrorCode +} from './proto/protofile' + +import { ListOperation, UpdateHandlerPayload, UserOperation } from './types' +import type { AudienceSettings, Settings } from './generated-types' + +export const buildHeaders = (audienceSettings: AudienceSettings | undefined, settings: Settings) => { + if (!audienceSettings || !settings) { + throw new IntegrationError('Bad Request', 'INVALID_REQUEST_DATA', 400) + } + + return { + // @ts-ignore - TS doesn't know about the oauth property + Authorization: `Bearer ${settings?.oauth?.accessToken}`, + 'Content-Type': 'application/json', + 'Login-Customer-Id': `products/${audienceSettings.accountType}/customers/${audienceSettings?.advertiserId}` + } +} + +export const assembleRawOps = (payload: UpdateHandlerPayload, operation: ListOperation): UserOperation[] => { + const rawOperations = [] + const audienceId = parseInt(payload.external_audience_id.split('/').pop() || '-1') + const isDelete = operation === 'remove' ? true : false + + if (payload.google_gid) { + rawOperations.push({ + UserId: payload.google_gid, + UserIdType: UserIdType.GOOGLE_USER_ID, + UserListId: audienceId, + Delete: isDelete + }) + } + + if (payload.mobile_advertising_id) { + const isIDFA = payload.mobile_advertising_id.includes('-') + + rawOperations.push({ + UserId: payload.mobile_advertising_id, + UserIdType: isIDFA ? UserIdType.IDFA : UserIdType.ANDROID_ADVERTISING_ID, + UserListId: audienceId, + Delete: isDelete + }) + } + + if (payload.anonymous_id) { + rawOperations.push({ + UserId: payload.anonymous_id, + UserIdType: UserIdType.PARTNER_PROVIDED_ID, + UserListId: audienceId, + Delete: isDelete + }) + } + + return rawOperations +} + +const handleErrorCode = ( + errorCodeString: string, + r: UpdateUsersDataResponse, + statsName: string, + statsContext: StatsContext | undefined +) => { + if (errorCodeString === 'PARTIAL_SUCCESS') { + statsContext?.statsClient.incr(`${statsName}.error.PARTIAL_SUCCESS`, 1, statsContext?.tags) + r.errors?.forEach((e) => { + if (e.errorCode) { + statsContext?.statsClient.incr(`${statsName}.error.${ErrorCode[e.errorCode]}`, 1, statsContext?.tags) + } + }) + } else { + statsContext?.statsClient.incr(`${statsName}.error.${errorCodeString}`, 1, statsContext?.tags) + } +} + +export const bulkUploaderResponseHandler = async ( + response: Response, + statsName: string, + statsContext: StatsContext | undefined +) => { + if (!response || !response.body) { + throw new IntegrationError(`Something went wrong unpacking the protobuf response`, 'INVALID_REQUEST_DATA', 400) + } + + const responseHandler = new UpdateUsersDataResponse() + const buffer = await response.arrayBuffer() + const protobufResponse = Buffer.from(buffer) + + const r = responseHandler.fromBinary(protobufResponse) + const errorCode = r.status as ErrorCode + const errorCodeString = ErrorCode[errorCode] || 'UNKNOWN_ERROR' + + if (errorCodeString === 'NO_ERROR' || response.status === 200) { + statsContext?.statsClient.incr(`${statsName}.success`, 1, statsContext?.tags) + } else { + handleErrorCode(errorCodeString, r, statsName, statsContext) + // Only internal errors shall be retried as they imply a temporary issue. + // The rest of the errors are permanent and shall be discarded. + // This emulates the legacy behavior of the DV360 destination. + if (errorCode === ErrorCode.INTERNAL_ERROR) { + statsContext?.statsClient.incr(`${statsName}.error.INTERNAL_ERROR`, 1, statsContext?.tags) + throw new IntegrationError('Bulk Uploader Internal Error', 'INTERNAL_SERVER_ERROR', 500) + } + } +} + +// To interact with the bulk uploader, we need to create a protobuf object as defined in the proto file. +// This method takes the raw payload and creates the protobuf object. +export const createUpdateRequest = ( + payload: UpdateHandlerPayload[], + operation: 'add' | 'remove' +): UpdateUsersDataRequest => { + const updateRequest = new UpdateUsersDataRequest() + + payload.forEach((p) => { + const rawOps = assembleRawOps(p, operation) + + // Every ID will generate an operation. + // That means that if google_gid, mobile_advertising_id, and anonymous_id are all present, we will create 3 operations. + // This emulates the legacy behavior of the DV360 destination. + rawOps.forEach((rawOp) => { + const op = new UserDataOperation({ + userId: rawOp.UserId, + userIdType: rawOp.UserIdType, + userListId: BigInt(rawOp.UserListId), + delete: !!rawOp.Delete + }) + + if (!op) { + throw new Error('Unable to create UserDataOperation') + } + + updateRequest.ops.push(op) + }) + }) + + return updateRequest +} + +export const sendUpdateRequest = async ( + request: RequestClient, + updateRequest: UpdateUsersDataRequest, + statsName: string, + statsContext: StatsContext | undefined +) => { + const binaryOperation = updateRequest.toBinary() + + try { + const response = await request(USER_UPLOAD_ENDPOINT, { + headers: { 'Content-Type': 'application/octet-stream' }, + body: binaryOperation, + method: 'POST' + }) + + await bulkUploaderResponseHandler(response, statsName, statsContext) + } catch (error) { + if (error.response?.status === 500) { + throw new IntegrationError(error.response.message, 'INTERNAL_SERVER_ERROR', 500) + } + + await bulkUploaderResponseHandler(error.response, statsName, statsContext) + } +} + +export const handleUpdate = async ( + request: RequestClient, + payload: UpdateHandlerPayload[], + operation: 'add' | 'remove', + statsContext: StatsContext | undefined +) => { + const statsName = operation === 'add' ? 'addToAudience' : 'removeFromAudience' + statsContext?.statsClient?.incr(`${statsName}.call`, 1, statsContext?.tags) + + const updateRequest = createUpdateRequest(payload, operation) + + if (updateRequest.ops.length !== 0) { + await sendUpdateRequest(request, updateRequest, statsName, statsContext) + } else { + statsContext?.statsClient.incr(`${statsName}.discard`, 1, statsContext?.tags) + } + + return { + status: 200 + } +} diff --git a/packages/destination-actions/src/destinations/display-video-360/types.ts b/packages/destination-actions/src/destinations/display-video-360/types.ts new file mode 100644 index 0000000000..e5065bcffe --- /dev/null +++ b/packages/destination-actions/src/destinations/display-video-360/types.ts @@ -0,0 +1,32 @@ +import type { Payload as AddToAudiencePayload } from './addToAudience/generated-types' +import type { Payload as RemoveFromAudiencePayload } from './removeFromAudience/generated-types' + +export interface RefreshTokenResponse { + access_token: string +} + +export interface GoogleAPIError { + response: { + status: number + data: { + error: { + message: string + } + } + } +} + +export type BasicListTypeMap = { + basicUserList: any + [key: string]: any +} + +export type UserOperation = { + UserId: string + UserIdType: number + UserListId: number + Delete: boolean +} + +export type ListOperation = 'add' | 'remove' +export type UpdateHandlerPayload = AddToAudiencePayload & RemoveFromAudiencePayload diff --git a/yarn.lock b/yarn.lock index 130d9e157c..2a6a2154ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1021,6 +1021,70 @@ resolved "https://registry.yarnpkg.com/@braze/web-sdk/-/web-sdk-4.7.0.tgz#5adb930690d78dd3bc77a93dececde360a08d0f7" integrity sha512-fYdCyjlZqBswlebO8XmbPj04soLycHxnSvCQ/bWpi4OB00fz/ne34vv1LzIP3d0V5++jwjsutxdEi5mRiiMK1Q== +"@bufbuild/buf-darwin-arm64@1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.28.0.tgz#2a891aed84a6220628e802f9f3feb11023877e32" + integrity sha512-trbnKKCINrRUXf0Rs88QmniZeQ4ODdsA9yISOj5JdeVDr9rQf1j/P2NUM+JimQpLm78I1CRR5qQPIX3Q8xR0NA== + +"@bufbuild/buf-darwin-x64@1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-x64/-/buf-darwin-x64-1.28.0.tgz#31c8037565a5ebee2cec09e9d2810bdc4e98550a" + integrity sha512-AVhGVJjaR26Qe0gNv+3AijeaQABJyFY8tlInEOOtTaQi2UGR8cgQoYBCziL3NQfH06wi7nEwTJm/ej2JUpAt2Q== + +"@bufbuild/buf-linux-aarch64@1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-aarch64/-/buf-linux-aarch64-1.28.0.tgz#6905429c44eb07bfbdd836f6c2d67648acea73ac" + integrity sha512-5qzLdO2MpXHPh2W5Rlf58oW8iH+wwOIjQs3vlgtgeI0nGu0FhRgpcrJ1E0lswTUE0NW19RMLI+q/QpjEl9W3aw== + +"@bufbuild/buf-linux-x64@1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-linux-x64/-/buf-linux-x64-1.28.0.tgz#26e74ab66808c26f807d650e0608b29be8bfa3a1" + integrity sha512-gZm7vGjhcabb1Zqp+ARYiJYCNm2mtbXFqo9Cqy0hpaonWaKAn7lbjtT0tG7rVX++QIfOpE3CwnjUNHck5xKP6A== + +"@bufbuild/buf-win32-arm64@1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-arm64/-/buf-win32-arm64-1.28.0.tgz#95eac3371f322f4df5291138bf87cf8474b39e26" + integrity sha512-ptIfiTYW2cMlPOcnkz3YF/aSR9ztAzeozycv460qDR0p0c0KYHKRTTFKD8TRahoyU7znmWEluYBdmKZAbbtwKg== + +"@bufbuild/buf-win32-x64@1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf-win32-x64/-/buf-win32-x64-1.28.0.tgz#af811f625cced53dc52e0127489ae96f1a9ca56b" + integrity sha512-vwjMUfelrB8RD/xHdR6MVEl9XqIxvASvzj0szz70hvQmzmU4BEOEUTHtERMOnBJxiPE1a28YEfpUlxyvm+COCg== + +"@bufbuild/buf@^1.28.0": + version "1.28.0" + resolved "https://registry.yarnpkg.com/@bufbuild/buf/-/buf-1.28.0.tgz#306fa54101597eec92e71d892a9f0a696624c546" + integrity sha512-QizjughxiWj53BTFijxQN5YDCcIriLAsCSYgxk+l9YzEC3hHAjCBen0hGJcSezWDLKWiGxAD6AMXiRIfAHKZjQ== + optionalDependencies: + "@bufbuild/buf-darwin-arm64" "1.28.0" + "@bufbuild/buf-darwin-x64" "1.28.0" + "@bufbuild/buf-linux-aarch64" "1.28.0" + "@bufbuild/buf-linux-x64" "1.28.0" + "@bufbuild/buf-win32-arm64" "1.28.0" + "@bufbuild/buf-win32-x64" "1.28.0" + +"@bufbuild/protobuf@1.4.2", "@bufbuild/protobuf@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@bufbuild/protobuf/-/protobuf-1.4.2.tgz#dc4faf21264a47b71a15806616043cb006e80ac8" + integrity sha512-JyEH8Z+OD5Sc2opSg86qMHn1EM1Sa+zj/Tc0ovxdwk56ByVNONJSabuCUbLQp+eKN3rWNfrho0X+3SEqEPXIow== + +"@bufbuild/protoc-gen-es@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@bufbuild/protoc-gen-es/-/protoc-gen-es-1.4.2.tgz#00c8b09430dd1154e626da7c247fd6425a1cd41d" + integrity sha512-/It7M2s8H1zTDvUMJu6vhBmtnzeFL2VS6e78RYIY38602pNXDK/vbteKUo4KrG0O07lOPFu87hHZ0Y+w5Ib6iw== + dependencies: + "@bufbuild/protobuf" "^1.4.2" + "@bufbuild/protoplugin" "1.4.2" + +"@bufbuild/protoplugin@1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@bufbuild/protoplugin/-/protoplugin-1.4.2.tgz#abf9b0e6a3dc8b52b1d6699d7a1ce5219fa82322" + integrity sha512-5IwGC1ZRD2A+KydGXeaSOErwfILLqVtvMH/RkN+cOoHcQd4EYXFStcF7g7aR+yICRDEEjQVi5tQF/qPGBSr9vg== + dependencies: + "@bufbuild/protobuf" "1.4.2" + "@typescript/vfs" "^1.4.0" + typescript "4.5.2" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -4208,6 +4272,13 @@ "@typescript-eslint/types" "5.1.0" eslint-visitor-keys "^3.0.0" +"@typescript/vfs@^1.4.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@typescript/vfs/-/vfs-1.5.0.tgz#ed942922724f9ace8c07c80b006c47e5e3833218" + integrity sha512-AJS307bPgbsZZ9ggCT3wwpg3VbTKMFNHfaY/uF0ahSkYYrPF2dSSKDNIDIQAHm9qJqbLvCsSJH7yN4Vs/CsMMg== + dependencies: + debug "^4.1.1" + "@wdio/cli@^7.26.0": version "7.30.0" resolved "https://registry.yarnpkg.com/@wdio/cli/-/cli-7.30.0.tgz#974a1d0763c077902786c71934cf72f3b0bc4804" @@ -16432,6 +16503,11 @@ typescript@4.3.5: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.2.tgz#8ac1fba9f52256fdb06fb89e4122fa6a346c2998" + integrity sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw== + "typescript@^3 || ^4": version "4.9.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" From 7b0fe448bc1b3661954e9a6ebbf480ac587eb3a9 Mon Sep 17 00:00:00 2001 From: Ankit Gupta <139338151+AnkitSegment@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:25:49 +0530 Subject: [PATCH 09/34] [STRATCONN] Klaviyo AddToProfileList and RemoveFromProfileList actions with engage setup (#1723) * AddToProfileList and RemoveFromProfileList action added * added new flow of addProfileToList and RemoveProfileFromList actions in klaviyo * completed audience setup for klaviyo * updated api key field for klaviyo * change in remove from list api test cases * full audience sync turned off for audience klaviyo * changing default path of external_id * Removed debug codes * code refactored * change in addProfile and removeProfile in klaviyo * Resolved build error * Resolved build error * Added buildHeaders function * Seperated addProfile and RemoveProfile Functions * Added audience desctiption * Change error handling of createProfile function * Change error handling of createProfile function * Modified in addProfileToList Test case --- .../__snapshots__/snapshot.test.ts.snap | 13 ++ .../klaviyo/__tests__/index.test.ts | 66 +++++++++- .../klaviyo/__tests__/snapshot.test.ts | 5 +- .../addProfileToList/__tests__/index.test.ts | 121 ++++++++++++++++++ .../addProfileToList/generated-types.ts | 12 ++ .../klaviyo/addProfileToList/index.ts | 25 ++++ .../src/destinations/klaviyo/functions.ts | 89 ++++++++++--- .../src/destinations/klaviyo/index.ts | 70 ++++++++-- .../src/destinations/klaviyo/properties.ts | 22 ++++ .../__tests__/index.test.ts | 72 +++++++++++ .../removeProfileFromList/generated-types.ts | 12 ++ .../klaviyo/removeProfileFromList/index.ts | 29 +++++ .../src/destinations/klaviyo/types.ts | 11 +- .../klaviyo/upsertProfile/index.ts | 4 +- 14 files changed, 515 insertions(+), 36 deletions(-) create mode 100644 packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts create mode 100644 packages/destination-actions/src/destinations/klaviyo/properties.ts create mode 100644 packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap index 51139559f6..b3e4f1a064 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,5 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Testing snapshot for actions-klaviyo destination: addProfileToList action - all fields 1`] = ` +Object { + "data": Object { + "attributes": Object { + "email": "mudwoz@zo.ad", + }, + "type": "profile", + }, +} +`; + exports[`Testing snapshot for actions-klaviyo destination: orderCompleted action - all fields 1`] = ` Object { "data": Object { @@ -35,6 +46,8 @@ Object { } `; +exports[`Testing snapshot for actions-klaviyo destination: removeProfileFromList action - all fields 1`] = `""`; + exports[`Testing snapshot for actions-klaviyo destination: trackEvent action - all fields 1`] = ` Object { "data": Object { diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/index.test.ts index e95aee2dbc..f2377d87ee 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/index.test.ts @@ -1,5 +1,5 @@ import nock from 'nock' -import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { IntegrationError, createTestEvent, createTestIntegration } from '@segment/actions-core' import Definition from '../index' const testDestination = createTestIntegration(Definition) @@ -11,6 +11,22 @@ export const settings = { api_key: apiKey } +const createAudienceInput = { + settings: { + api_key: '' + }, + audienceName: '' +} + +const getAudienceInput = { + settings: { + api_key: apiKey + }, + externalId: 'XYZABC' +} + +const audienceName = 'Klaviyo Audience Name' + describe('Klaviyo (actions)', () => { describe('testAuthentication', () => { it('should validate authentication inputs', async () => { @@ -52,4 +68,52 @@ describe('Klaviyo (actions)', () => { } }) }) + + describe('createAudience', () => { + it('should fail if no audience name is set', async () => { + await expect(testDestination.createAudience(createAudienceInput)).rejects.toThrowError(IntegrationError) + }) + + it('should fail if no api key is set in settings', async () => { + await expect(testDestination.createAudience(createAudienceInput)).rejects.toThrowError(IntegrationError) + }) + + it('creates an audience', async () => { + createAudienceInput.audienceName = audienceName + createAudienceInput.settings.api_key = apiKey + + nock(`${API_URL}`) + .post('/lists', { data: { type: 'list', attributes: { name: audienceName } } }) + .matchHeader('Authorization', `Klaviyo-API-Key ${apiKey}`) + .reply(200, { + success: true, + data: { + id: 'XYZABC' + } + }) + + const r = await testDestination.createAudience(createAudienceInput) + expect(r).toEqual({ + externalId: 'XYZABC' + }) + }) + }) + + describe('getAudience', () => { + const listId = getAudienceInput.externalId + it('should succeed when with valid list id', async () => { + nock(`${API_URL}/lists`) + .get(`/${listId}`) + .reply(200, { + success: true, + data: { + id: 'XYZABC' + } + }) + const r = await testDestination.getAudience(getAudienceInput) + expect(r).toEqual({ + externalId: 'XYZABC' + }) + }) + }) }) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts index da750935bf..85b0b1504a 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/snapshot.test.ts @@ -13,13 +13,12 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => { const action = destination.actions[actionSlug] const [eventData, settingsData] = generateTestData(seedName, destination, action, false) - nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().get(/.*/).reply(200, {}) nock(/.*/) .persist() .post(/.*/) .reply(200, { data: { id: 'fake-id' } }) - nock(/.*/).persist().put(/.*/).reply(200) - + nock(/.*/).persist().put(/.*/).reply(200, {}) const event = createTestEvent({ properties: eventData }) diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts new file mode 100644 index 0000000000..90e1d4a27a --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts @@ -0,0 +1,121 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Definition from '../../index' +import { API_URL } from '../../config' +import { AggregateAjvError } from '@segment/ajv-human-errors' + +const testDestination = createTestIntegration(Definition) + +const apiKey = 'fake-api-key' + +export const settings = { + api_key: apiKey +} +const listId = 'XYZABC' + +const requestBody = { + data: [ + { + type: 'profile', + id: 'XYZABC' + } + ] +} + +const profileData = { + data: { + type: 'profile', + attributes: { + email: 'demo@segment.com' + } + } +} + +describe('Add List To Profile', () => { + it('should throw error if no list_id/email is provided', async () => { + const event = createTestEvent({ + type: 'track', + properties: {} + }) + + await expect(testDestination.testAction('addProfileToList', { event, settings })).rejects.toThrowError( + AggregateAjvError + ) + }) + + it('should add profile to list if successful', async () => { + nock(`${API_URL}`) + .post('/profiles/', profileData) + .reply(200, { + data: { + id: 'XYZABC' + } + }) + + nock(`${API_URL}/lists/${listId}`) + .post('/relationships/profiles/', requestBody) + .reply( + 200, + JSON.stringify({ + content: requestBody + }) + ) + + const event = createTestEvent({ + type: 'track', + userId: '123', + traits: { + email: 'demo@segment.com' + } + }) + const mapping = { + external_id: listId, + email: { + '@path': '$.traits.email' + } + } + await expect( + testDestination.testAction('addProfileToList', { event, mapping, settings }) + ).resolves.not.toThrowError() + }) + + it('should add to list if profile is already created', async () => { + nock(`${API_URL}`) + .post('/profiles/', profileData) + .reply(409, { + errors: [ + { + meta: { + duplicate_profile_id: 'XYZABC' + } + } + ] + }) + + nock(`${API_URL}/lists/${listId}`) + .post('/relationships/profiles/', requestBody) + .reply( + 200, + JSON.stringify({ + content: requestBody + }) + ) + + const event = createTestEvent({ + type: 'track', + userId: '123', + traits: { + email: 'demo@segment.com' + } + }) + const mapping = { + external_id: listId, + email: { + '@path': '$.traits.email' + } + } + await expect( + testDestination.testAction('addProfileToList', { event, mapping, settings }) + ).resolves.not.toThrowError() + }) +}) diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts new file mode 100644 index 0000000000..dac9c71807 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts @@ -0,0 +1,12 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The user's email to send to Klavio. + */ + email?: string + /** + * 'Insert the ID of the default list that you'd like to subscribe users to when you call .identify().' + */ + external_id: string +} diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts new file mode 100644 index 0000000000..6167cc3ff0 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts @@ -0,0 +1,25 @@ +import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import { Payload } from './generated-types' +import { createProfile, addProfileToList } from '../functions' +import { email, external_id } from '../properties' + +const action: ActionDefinition = { + title: 'Add Profile To List', + description: 'Add Profile To List', + defaultSubscription: 'event = "Audience Entered"', + fields: { + email: { ...email }, + external_id: { ...external_id } + }, + perform: async (request, { payload }) => { + const { email, external_id } = payload + if (!email) { + throw new PayloadValidationError('Missing Email') + } + const profileId = await createProfile(request, email) + return await addProfileToList(request, profileId, external_id) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index 3801495e37..c7fe48c833 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -1,21 +1,13 @@ -import { RequestClient, DynamicFieldResponse, APIError } from '@segment/actions-core' +import { APIError, RequestClient, DynamicFieldResponse } from '@segment/actions-core' import { API_URL, REVISION_DATE } from './config' -import { GetListResultContent } from './types' -import { Settings } from './generated-types' +import { KlaviyoAPIError, ListIdResponse, ProfileData, listData } from './types' -export async function getListIdDynamicData(request: RequestClient, settings: Settings): Promise { +export async function getListIdDynamicData(request: RequestClient): Promise { try { - const result = await request(`${API_URL}/lists/`, { - method: 'get', - headers: { - Authorization: `Klaviyo-API-Key ${settings.api_key}`, - Accept: 'application/json', - revision: REVISION_DATE - }, - skipResponseCloning: true + const result: ListIdResponse = await request(`${API_URL}/lists/`, { + method: 'get' }) - const parsedContent = JSON.parse(result.content) as GetListResultContent - const choices = parsedContent.data.map((list: { id: string; attributes: { name: string } }) => { + const choices = JSON.parse(result.content).data.map((list: { id: string; attributes: { name: string } }) => { return { value: list.id, label: list.attributes.name } }) return { @@ -33,18 +25,77 @@ export async function getListIdDynamicData(request: RequestClient, settings: Set } } -export async function addProfileToList(request: RequestClient, profileId: string, listId: string) { - const listData = { +export async function addProfileToList(request: RequestClient, id: string, list_id: string | undefined) { + const listData: listData = { data: [ { type: 'profile', - id: profileId + id: id } ] } - - await request(`${API_URL}/lists/${listId}/relationships/profiles/`, { + const list = await request(`${API_URL}/lists/${list_id}/relationships/profiles/`, { method: 'POST', json: listData }) + return list +} + +export async function removeProfileFromList(request: RequestClient, id: string, list_id: string | undefined) { + const listData: listData = { + data: [ + { + type: 'profile', + id: id + } + ] + } + const list = await request(`${API_URL}/lists/${list_id}/relationships/profiles/`, { + method: 'DELETE', + json: listData + }) + + return list +} + +export async function getProfile(request: RequestClient, email: string) { + const profile = await request(`${API_URL}/profiles/?filter=equals(email,"${email}")`, { + method: 'GET' + }) + return profile.json() +} + +export async function createProfile(request: RequestClient, email: string) { + try { + const profileData: ProfileData = { + data: { + type: 'profile', + attributes: { + email + } + } + } + + const profile = await request(`${API_URL}/profiles/`, { + method: 'POST', + json: profileData + }) + const rs = await profile.json() + return rs.data.id + } catch (error) { + const { response } = error as KlaviyoAPIError + if (response.status == 409) { + const rs = await response.json() + return rs.errors[0].meta.duplicate_profile_id + } + } +} + +export function buildHeaders(authKey: string) { + return { + Authorization: `Klaviyo-API-Key ${authKey}`, + Accept: 'application/json', + revision: REVISION_DATE, + 'Content-Type': 'application/json' + } } diff --git a/packages/destination-actions/src/destinations/klaviyo/index.ts b/packages/destination-actions/src/destinations/klaviyo/index.ts index 2efaca325e..e006457426 100644 --- a/packages/destination-actions/src/destinations/klaviyo/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/index.ts @@ -1,14 +1,15 @@ -import type { DestinationDefinition } from '@segment/actions-core' +import { IntegrationError, AudienceDestinationDefinition, PayloadValidationError } from '@segment/actions-core' import type { Settings } from './generated-types' +import { API_URL } from './config' import upsertProfile from './upsertProfile' -import { API_URL, REVISION_DATE } from './config' - +import addProfileToList from './addProfileToList' +import removeProfileFromList from './removeProfileFromList' import trackEvent from './trackEvent' - import orderCompleted from './orderCompleted' +import { buildHeaders } from './functions' -const destination: DestinationDefinition = { +const destination: AudienceDestinationDefinition = { name: 'Klaviyo (Actions)', slug: 'actions-klaviyo', mode: 'cloud', @@ -51,16 +52,65 @@ const destination: DestinationDefinition = { extendRequest({ settings }) { return { - headers: { - Authorization: `Klaviyo-API-Key ${settings.api_key}`, - Accept: 'application/json', - revision: REVISION_DATE - } + headers: buildHeaders(settings.api_key) } }, + audienceFields: {}, + audienceConfig: { + mode: { + type: 'synced', + full_audience_sync: false + }, + async createAudience(request, createAudienceInput) { + const audienceName = createAudienceInput.audienceName + const apiKey = createAudienceInput.settings.api_key + if (!audienceName) { + throw new PayloadValidationError('Missing audience name value') + } + + if (!apiKey) { + throw new PayloadValidationError('Missing Api Key value') + } + + const response = await request(`${API_URL}/lists`, { + method: 'POST', + headers: buildHeaders(apiKey), + json: { + data: { type: 'list', attributes: { name: audienceName } } + } + }) + const r = await response.json() + return { + externalId: r.data.id + } + }, + async getAudience(request, getAudienceInput) { + const listId = getAudienceInput.externalId + const apiKey = getAudienceInput.settings.api_key + const response = await request(`${API_URL}/lists/${listId}`, { + method: 'GET', + headers: buildHeaders(apiKey) + }) + const r = await response.json() + const externalId = r.data.id + if (externalId !== getAudienceInput.externalId) { + throw new IntegrationError( + "Unable to verify ownership over audience. Segment Audience ID doesn't match The Klaviyo List Id.", + 'INVALID_REQUEST_DATA', + 400 + ) + } + + return { + externalId + } + } + }, actions: { upsertProfile, + addProfileToList, + removeProfileFromList, trackEvent, orderCompleted } diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts new file mode 100644 index 0000000000..ae15fdb5b5 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -0,0 +1,22 @@ +import { InputField } from '@segment/actions-core/destination-kit/types' + +export const external_id: InputField = { + label: 'External Id', + description: `'Insert the ID of the default list that you'd like to subscribe users to when you call .identify().'`, + type: 'string', + default: { + '@path': '$.context.personas.external_audience_id' + }, + unsafe_hidden: true, + required: true +} + +export const email: InputField = { + label: 'Email', + description: `The user's email to send to Klavio.`, + type: 'string', + default: { + '@path': '$.context.traits.email' + }, + readOnly: true +} diff --git a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts new file mode 100644 index 0000000000..3117aff251 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts @@ -0,0 +1,72 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import Definition from '../../index' +import { API_URL } from '../../config' +import { AggregateAjvError } from '@segment/ajv-human-errors' + +const testDestination = createTestIntegration(Definition) + +const apiKey = 'fake-api-key' + +export const settings = { + api_key: apiKey +} +const listId = 'XYZABC' + +describe('Remove List from Profile', () => { + it('should throw error if no external_id/email is provided', async () => { + const event = createTestEvent({ + type: 'track', + properties: {} + }) + + await expect(testDestination.testAction('removeProfileFromList', { event, settings })).rejects.toThrowError( + AggregateAjvError + ) + }) + + it('should remove profile from list if successful', async () => { + const requestBody = { + data: [ + { + type: 'profile', + id: 'XYZABC' + } + ] + } + + const email = 'test@example.com' + nock(`${API_URL}/profiles`) + .get(`/?filter=equals(email,"${email}")`) + .reply(200, { + data: [{ id: 'XYZABC' }] + }) + + nock(`${API_URL}/lists/${listId}`) + .delete('/relationships/profiles/', requestBody) + .reply(200, { + data: [ + { + id: 'XYZABC' + } + ] + }) + + const event = createTestEvent({ + type: 'track', + userId: '123', + context: { + personas: { + external_audience_id: listId + }, + traits: { + email: 'test@example.com' + } + } + }) + + await expect( + testDestination.testAction('removeProfileFromList', { event, settings, useDefaultMappings: true }) + ).resolves.not.toThrowError() + }) +}) diff --git a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts new file mode 100644 index 0000000000..dac9c71807 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts @@ -0,0 +1,12 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The user's email to send to Klavio. + */ + email?: string + /** + * 'Insert the ID of the default list that you'd like to subscribe users to when you call .identify().' + */ + external_id: string +} diff --git a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts new file mode 100644 index 0000000000..12a96756e4 --- /dev/null +++ b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts @@ -0,0 +1,29 @@ +import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import { Payload } from './generated-types' + +import { getProfile, removeProfileFromList } from '../functions' +import { email, external_id } from '../properties' + +const action: ActionDefinition = { + title: 'Remove profile from list', + description: 'Remove profile from list', + defaultSubscription: 'event = "Audience Exited"', + fields: { + email: { ...email }, + external_id: { ...external_id } + }, + perform: async (request, { payload }) => { + const { email, external_id } = payload + if (!email) { + throw new PayloadValidationError('Missing Email') + } + const profileData = await getProfile(request, email) + const v = profileData.data + if (v && v.length !== 0) { + return await removeProfileFromList(request, v[0].id, external_id) + } + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index 57ad5a07df..db2fe1a272 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -1,5 +1,4 @@ import { HTTPError } from '@segment/actions-core' - export class KlaviyoAPIError extends HTTPError { response: Response & { data: { @@ -63,6 +62,16 @@ export interface EventData { } } +export interface listData { + data: { + type: string + id?: string + }[] +} + +export interface ListIdResponse { + content: string +} export interface GetListResultContent { data: { id: string diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts index 396b967eda..8aff65c904 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts @@ -130,8 +130,8 @@ const action: ActionDefinition = { } }, dynamicFields: { - list_id: async (request, { settings }): Promise => { - return getListIdDynamicData(request, settings) + list_id: async (request): Promise => { + return getListIdDynamicData(request) } }, perform: async (request, { payload }) => { From 8e9e7762c5fd3d58118fe76809f323decc61abf5 Mon Sep 17 00:00:00 2001 From: Elena Date: Wed, 29 Nov 2023 04:02:37 -0800 Subject: [PATCH 10/34] Yahoo audiences 5 (#1742) * new audience settings, logging * mapping change, device type --- .../yahoo-audiences/__tests__/index.test.ts | 23 +---- .../yahoo-audiences/generated-types.ts | 8 +- .../src/destinations/yahoo-audiences/index.ts | 85 +++++-------------- .../updateSegment/generated-types.ts | 8 +- .../yahoo-audiences/updateSegment/index.ts | 64 +++++--------- .../destinations/yahoo-audiences/utils-rt.ts | 64 +++----------- .../destinations/yahoo-audiences/utils-tax.ts | 4 +- 7 files changed, 70 insertions(+), 186 deletions(-) diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/__tests__/index.test.ts b/packages/destination-actions/src/destinations/yahoo-audiences/__tests__/index.test.ts index 2ccf26ddf2..5795e0f071 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/__tests__/index.test.ts @@ -19,8 +19,10 @@ const createAudienceInput = { }, audienceName: '', audienceSettings: { - audience_key: AUDIENCE_KEY, - audience_id: AUDIENCE_ID + personas: { + computation_key: AUDIENCE_KEY, + computation_id: AUDIENCE_ID + } } } @@ -28,7 +30,6 @@ describe('Yahoo Audiences', () => { describe('createAudience() function', () => { let testDestination: any const OLD_ENV = process.env - beforeEach(() => { jest.resetModules() // Most important - it clears the cache process.env = { ...OLD_ENV } // Make a copy @@ -53,24 +54,8 @@ describe('Yahoo Audiences', () => { }) }) describe('Failure cases', () => { - it('should throw an error when audience_id setting is missing', async () => { - createAudienceInput.settings.engage_space_id = 'acme_corp_engage_space' - createAudienceInput.audienceSettings.audience_key = 'sneakeres_buyers' - createAudienceInput.audienceSettings.audience_id = '' - await expect(testDestination.createAudience(createAudienceInput)).rejects.toThrowError(IntegrationError) - }) - - it('should throw an error when audience_key setting is missing', async () => { - createAudienceInput.settings.engage_space_id = 'acme_corp_engage_space' - createAudienceInput.audienceSettings.audience_key = '' - createAudienceInput.audienceSettings.audience_id = 'aud_12345' - await expect(testDestination.createAudience(createAudienceInput)).rejects.toThrowError(IntegrationError) - }) - it('should throw an error when engage_space_id setting is missing', async () => { createAudienceInput.settings.engage_space_id = '' - createAudienceInput.audienceSettings.audience_key = 'sneakeres_buyers' - createAudienceInput.audienceSettings.audience_id = 'aud_123456789012345678901234567' await expect(testDestination.createAudience(createAudienceInput)).rejects.toThrowError(IntegrationError) }) }) diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/generated-types.ts b/packages/destination-actions/src/destinations/yahoo-audiences/generated-types.ts index 9168142951..fe1b01a1fb 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/generated-types.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/generated-types.ts @@ -18,11 +18,7 @@ export interface Settings { export interface AudienceSettings { /** - * Segment Audience Id (aud_...) + * Placeholder field to allow the audience to be created. Do not change this */ - audience_id: string - /** - * Segment Audience Key - */ - audience_key: string + placeholder?: boolean } diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/index.ts b/packages/destination-actions/src/destinations/yahoo-audiences/index.ts index 9b40abfbfb..5b6e4f1f54 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/index.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/index.ts @@ -4,7 +4,11 @@ import type { Settings, AudienceSettings } from './generated-types' import { generate_jwt } from './utils-rt' import updateSegment from './updateSegment' import { gen_customer_taxonomy_payload, gen_segment_subtaxonomy_payload, update_taxonomy } from './utils-tax' - +type PersonasSettings = { + computation_id: string + computation_key: string + parent_id: string +} interface RefreshTokenResponse { access_token: string } @@ -51,7 +55,7 @@ const destination: AudienceDestinationDefinition = { const body_form_data = gen_customer_taxonomy_payload(settings) // The last 2 params are undefined because we don't have statsContext.statsClient and statsContext.tags in testAuthentication() - await update_taxonomy('', tx_creds, request, body_form_data, undefined, undefined) + return await update_taxonomy('', tx_creds, request, body_form_data, undefined, undefined) }, refreshAccessToken: async (request, { auth }) => { // Refresh Realtime API token (Oauth2 client_credentials) @@ -85,35 +89,14 @@ const destination: AudienceDestinationDefinition = { } }, audienceFields: { - audience_id: { - type: 'string', - label: 'Audience Id', - description: 'Segment Audience Id (aud_...)', - required: true - }, - audience_key: { - label: 'Audience key', - description: 'Segment Audience Key', - type: 'string', - required: true + placeholder: { + type: 'boolean', + label: 'Placeholder Setting', + description: 'Placeholder field to allow the audience to be created. Do not change this', + default: true } - /*, - identifier: { - label: 'User Identifier', - description: 'Specify the identifier(s) to send to Yahoo', - type: 'string', - required: true, - default: 'email', - choices: [ - { value: 'email', label: 'Send email' }, - { value: 'maid', label: 'Send MAID' }, - { value: 'phone', label: 'Send phone' }, - { value: 'email_maid', label: 'Send email and/or MAID' }, - { value: 'email_maid_phone', label: 'Send email, MAID and/or phone' }, - { value: 'email_phone', label: 'Send email and/or phone' }, - { value: 'phone_maid', label: 'Send phone and/or MAID' } - ] - }*/ + // This is a required object, but we don't need to define any fields + // Placeholder setting will be removed once we make AudienceSettings optional }, audienceConfig: { mode: { @@ -122,46 +105,19 @@ const destination: AudienceDestinationDefinition = { }, async createAudience(request, createAudienceInput) { - // const tax_client_key = JSON.parse(auth.clientId)['tax_api'] - - //engage_space_id, audience_id and audience_key will be removed once we have Payload accessible by createAudience() - //context.personas.computation_id - //context.personas.computation_key - //context.personas.namespace - const audience_id = createAudienceInput.audienceSettings?.audience_id - const audience_key = createAudienceInput.audienceSettings?.audience_key + const audienceSettings = createAudienceInput.audienceSettings + // @ts-ignore type is not defined, and we will define it later + const personas = audienceSettings.personas as PersonasSettings const engage_space_id = createAudienceInput.settings?.engage_space_id - //const identifier = createAudienceInput.audienceSettings?.identifier + const audience_id = personas.computation_id + const audience_key = personas.computation_key + const statsClient = createAudienceInput?.statsContext?.statsClient const statsTags = createAudienceInput?.statsContext?.tags - // The 3 errors below will be removed once we have Payload accessible by createAudience() - if (!audience_id) { - throw new IntegrationError( - 'Create Audience: missing audience setting "audience Id"', - 'MISSING_REQUIRED_FIELD', - 400 - ) - } - - if (!audience_key) { - throw new IntegrationError( - 'Create Audience: missing audience setting "audience key"', - 'MISSING_REQUIRED_FIELD', - 400 - ) - } if (!engage_space_id) { throw new IntegrationError('Create Audience: missing setting "Engage space Id" ', 'MISSING_REQUIRED_FIELD', 400) } - // Removed since identifier is inherited from the payload. Required Ids are sent when they're mapped in Configurable Id sync - // if (!identifier) { - // throw new IntegrationError( - // 'Create Audience: missing audience setting "Identifier"', - // 'MISSING_REQUIRED_FIELD', - // 400 - // ) - // } if (!process.env.ACTIONS_YAHOO_AUDIENCES_TAXONOMY_CLIENT_SECRET) { throw new IntegrationError('Missing Taxonomy API client secret', 'MISSING_REQUIRED_FIELD', 400) @@ -188,7 +144,8 @@ const destination: AudienceDestinationDefinition = { return { externalId: audience_id } }, async getAudience(_, getAudienceInput) { - const audience_id = getAudienceInput.audienceSettings?.audience_id + // getAudienceInput.externalId represents audience ID that was created in createAudience + const audience_id = getAudienceInput.externalId if (!audience_id) { throw new IntegrationError('Missing audience_id value', 'MISSING_REQUIRED_FIELD', 400) } diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/generated-types.ts b/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/generated-types.ts index 00baf34f15..3f18ee2219 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/generated-types.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/generated-types.ts @@ -19,6 +19,10 @@ export interface Payload { * Segment computation class used to determine if input event is from an Engage Audience'. Value must be = 'audience'. */ segment_computation_action: string + /** + * Phone number of a user + */ + phone?: string /** * Email address of a user */ @@ -27,10 +31,6 @@ export interface Payload { * User's mobile advertising Id */ advertising_id?: string - /** - * Phone number of a user - */ - phone?: string /** * User's mobile device type */ diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/index.ts b/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/index.ts index 951e1578b2..065668bb6e 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/index.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/updateSegment/index.ts @@ -56,11 +56,25 @@ const action: ActionDefinition = { }, choices: [{ label: 'audience', value: 'audience' }] }, + phone: { + label: 'User Phone', + description: 'Phone number of a user', + type: 'string', + unsafe_hidden: false, + required: false, + default: { + '@if': { + exists: { '@path': '$.traits.phone' }, + then: { '@path': '$.traits.phone' }, // Phone is sent as identify's trait or track's property + else: { '@path': '$.properties.phone' } + } + } + }, email: { label: 'User Email', description: 'Email address of a user', type: 'string', - unsafe_hidden: true, + unsafe_hidden: false, required: false, default: { '@if': { @@ -74,52 +88,22 @@ const action: ActionDefinition = { label: 'User Mobile Advertising ID', description: "User's mobile advertising Id", type: 'string', - unsafe_hidden: true, - default: { - '@path': '$.context.device.advertisingId' - }, - required: false - }, - phone: { - label: 'User Phone', - description: 'Phone number of a user', - type: 'string', - unsafe_hidden: true, + unsafe_hidden: false, required: false, default: { - '@if': { - exists: { '@path': '$.traits.phone' }, - then: { '@path': '$.traits.phone' }, // Phone is sent as identify's trait or track's property - else: { '@path': '$.properties.phone' } - } + '@path': '$.context.device.advertisingId' } }, device_type: { label: 'User Mobile Device Type', // This field is required to determine the type of the advertising Id: IDFA or GAID description: "User's mobile device type", type: 'string', - unsafe_hidden: true, + unsafe_hidden: false, + required: false, default: { '@path': '$.context.device.type' - }, - required: false + } }, - // identifier: { - // label: 'User Identifier', - // description: 'Specify the identifier(s) to send to Yahoo', - // type: 'string', - // required: true, - // default: 'email', - // choices: [ - // { value: 'email', label: 'Send email' }, - // { value: 'maid', label: 'Send MAID' }, - // { value: 'phone', label: 'Send phone' }, - // { value: 'email_maid', label: 'Send email and/or MAID' }, - // { value: 'email_maid_phone', label: 'Send email, MAID and/or phone' }, - // { value: 'email_phone', label: 'Send email and/or phone' }, - // { value: 'phone_maid', label: 'Send phone and/or MAID' } - // ] - // }, gdpr_flag: { label: 'GDPR Flag', description: 'Set to true to indicate that audience data is subject to GDPR regulations', @@ -138,12 +122,10 @@ const action: ActionDefinition = { perform: (request, { payload, auth, statsContext }) => { const rt_access_token = auth?.accessToken - //const rt_access_token = 'cc606d91-1786-47a0-87fd-6f48ee70fa7c' return process_payload(request, [payload], rt_access_token, statsContext) }, performBatch: (request, { payload, auth, statsContext }) => { const rt_access_token = auth?.accessToken - //const rt_access_token = 'cc606d91-1786-47a0-87fd-6f48ee70fa7c' return process_payload(request, payload, rt_access_token, statsContext) } } @@ -161,8 +143,8 @@ async function process_payload( // Send request to Yahoo only when all events in the batch include selected Ids if (body.data.length > 0) { if (statsClient && statsTag) { - statsClient?.incr('yahoo_audiences', 1, [...statsTag, 'action:updateSegmentTriggered']) - statsClient?.incr('yahoo_audiences', body.data.length, [...statsTag, 'action:updateSegmentRecordsSent']) + statsClient?.incr('updateSegmentTriggered', 1, statsTag) + statsClient?.incr('updateSegmentRecordsSent', body.data.length, statsTag) } return request('https://dataxonline.yahoo.com/online/audience/', { method: 'POST', @@ -173,7 +155,7 @@ async function process_payload( }) } else { if (statsClient && statsTag) { - statsClient?.incr('yahoo_audiences', 1, [...statsTag, 'action:updateSegmentDiscarded']) + statsClient?.incr('updateSegmentDiscarded', 1, statsTag) } throw new PayloadValidationError('Selected identifier(s) not available in the event(s)') } diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/utils-rt.ts b/packages/destination-actions/src/destinations/yahoo-audiences/utils-rt.ts index 821786f737..e62ce3314f 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/utils-rt.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/utils-rt.ts @@ -52,52 +52,7 @@ export function generate_jwt(client_id: string, client_secret: string): string { * @param payload The payload. * @returns {{ maid: boolean; email: boolean }} The definitions object (id_schema). */ -/* -// TODO: remove this function. We inherit the id_schema from the payload. Once a user has mapped an -// identifier in Configurable Id sync, we can use the identifier from the payload. -export function get_id_schema( - payload: Payload, - audienceSettings: AudienceSettings -): { maid: boolean; email: boolean; phone: boolean } { - const schema = { - email: false, - maid: false, - phone: false - } - let id_type - audienceSettings.identifier ? (id_type = audienceSettings.identifier) : (id_type = payload.identifier) - switch (id_type) { - case 'email': - schema.email = true - break - case 'maid': - schema.maid = true - break - case 'phone': - schema.phone = true - break - case 'email_maid': - schema.maid = true - schema.email = true - break - case 'email_maid_phone': - schema.maid = true - schema.email = true - schema.phone = true - break - case 'email_phone': - schema.email = true - schema.phone = true - break - case 'phone_maid': - schema.phone = true - schema.maid = true - break - } - return schema -} -*/ export function validate_phone(phone: string) { /* Phone must match E.164 format: a number up to 15 digits in length starting with a ‘+’ @@ -138,13 +93,22 @@ export function gen_update_segment_payload(payloads: Payload[]): YahooPayload { let idfa: string | undefined = '' let gpsaid: string | undefined = '' if (event.advertising_id) { - switch (event.device_type) { - case 'ios': + if (event.device_type) { + switch (event.device_type) { + case 'ios': + idfa = event.advertising_id + break + case 'android': + gpsaid = event.advertising_id + break + } + } else { + if (event.advertising_id === event.advertising_id.toUpperCase()) { + // Apple IDFA is always uppercase idfa = event.advertising_id - break - case 'android': + } else { gpsaid = event.advertising_id - break + } } } let hashed_phone: string | undefined = '' diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/utils-tax.ts b/packages/destination-actions/src/destinations/yahoo-audiences/utils-tax.ts index c34fb04a25..5c2ec3fc7b 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/utils-tax.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/utils-tax.ts @@ -89,13 +89,13 @@ export async function update_taxonomy( } }) if (statsClient && statsTags) { - statsClient.incr('yahoo_audiences', 1, [...statsTags, 'util:update_taxonomy.success']) + statsClient.incr('update_taxonomy.success', 1, statsTags) } return await add_segment_node.json() } catch (error) { const _error = error as { response: { data: unknown; status: string } } if (statsClient && statsTags) { - statsClient.incr('yahoo_audiences', 1, [...statsTags, `util:update_taxonomy.error_${_error.response.status}`]) + statsClient.incr('update_taxonomy.error', 1, statsTags) } // If Taxonomy API returned 401, throw Integration error w/status 400 to prevent refreshAccessToken from firing // Otherwise throw the original error From cbce8225e2c37095b41628a1c0d05580bd3c7265 Mon Sep 17 00:00:00 2001 From: Andrew Byrd Date: Wed, 29 Nov 2023 07:40:01 -0500 Subject: [PATCH 11/34] fix for empty string in setting (#1734) --- .../destinations/pendo-web-actions/src/loadScript.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts b/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts index ad4c7b8b36..97ca3820a7 100644 --- a/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts +++ b/packages/browser-destinations/destinations/pendo-web-actions/src/loadScript.ts @@ -16,7 +16,7 @@ export function loadPendo(apiKey, region, cnameContentHost) { })(v[w]) y = e.createElement(n) y.async = !0 - y.src = `${cnameContentHost ?? region}/agent/static/${apiKey}/pendo.js` + y.src = `${cnameContentHost || region}/agent/static/${apiKey}/pendo.js` z = e.getElementsByTagName(n)[0] z.parentNode.insertBefore(y, z) })(window, document, 'script', 'pendo') From 51e7e4442095d93dee8434541c5906cb632b57d6 Mon Sep 17 00:00:00 2001 From: Sam <22425976+imsamdez@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:41:05 +0100 Subject: [PATCH 12/34] fix(Jimo): resolve when jimo is not an array & remove manualInit from settings (#1740) * feat(sendUserData): handle traits * chore(clean): remove console.log * chore(eslint): remove warning * fix(Jimo): resolve when jimo is not an array & remove manualInit from settings * fix(Jimo): remove all ref to manualInit --- .../destinations/jimo/src/generated-types.ts | 4 ---- .../destinations/jimo/src/index.ts | 10 +--------- .../destinations/jimo/src/init-script.ts | 1 - 3 files changed, 1 insertion(+), 14 deletions(-) diff --git a/packages/browser-destinations/destinations/jimo/src/generated-types.ts b/packages/browser-destinations/destinations/jimo/src/generated-types.ts index 9809f5f4d1..89ab4c93fd 100644 --- a/packages/browser-destinations/destinations/jimo/src/generated-types.ts +++ b/packages/browser-destinations/destinations/jimo/src/generated-types.ts @@ -5,8 +5,4 @@ export interface Settings { * Id of the Jimo project. You can find the Project Id here: https://i.usejimo.com/settings/install/portal */ projectId: string - /** - * Toggling to true will prevent Jimo from initializing automatically. For more information, check out: https://help.usejimo.com/knowledge-base/for-developers/sdk-guides/manual-initialization - */ - manualInit?: boolean } diff --git a/packages/browser-destinations/destinations/jimo/src/index.ts b/packages/browser-destinations/destinations/jimo/src/index.ts index f061c4d7c9..fd23a11714 100644 --- a/packages/browser-destinations/destinations/jimo/src/index.ts +++ b/packages/browser-destinations/destinations/jimo/src/index.ts @@ -29,14 +29,6 @@ export const destination: BrowserDestinationDefinition = { label: 'Id', type: 'string', required: true - }, - manualInit: { - label: 'Initialize Jimo manually', - description: - 'Toggling to true will prevent Jimo from initializing automatically. For more information, check out: https://help.usejimo.com/knowledge-base/for-developers/sdk-guides/manual-initialization', - type: 'boolean', - required: false, - default: false } }, presets: [ @@ -53,7 +45,7 @@ export const destination: BrowserDestinationDefinition = { await deps.loadScript(`${ENDPOINT_UNDERCITY}`) - await deps.resolveWhen(() => typeof window.jimo.push === 'function', 100) + await deps.resolveWhen(() => Array.isArray(window.jimo), 100) return window.jimo as JimoSDK }, diff --git a/packages/browser-destinations/destinations/jimo/src/init-script.ts b/packages/browser-destinations/destinations/jimo/src/init-script.ts index ca345d03a0..a93d29eed8 100644 --- a/packages/browser-destinations/destinations/jimo/src/init-script.ts +++ b/packages/browser-destinations/destinations/jimo/src/init-script.ts @@ -8,5 +8,4 @@ export function initScript(settings: Settings) { window.jimo = [] window['JIMO_PROJECT_ID'] = settings.projectId - window['JIMO_MANUAL_INIT'] = settings.manualInit === true } From 3f950f957f0d60ad53992fc0972eb6f040d301ce Mon Sep 17 00:00:00 2001 From: Namit Arora <119914846+namit1Flow@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:12:02 +0530 Subject: [PATCH 13/34] create trackEvent and identifyUser browser-destinations for 1flow (#1703) * create trackEvent and identifyUser browser-destinations for 1flow * track and identify fixes * 1flow web fixes * generated-types fixes * Added Description For Destination * Added Destination Description * test issues fixes * lint and jest issues fixes --- .../destinations/1flow/README.md | 31 +++++++ .../destinations/1flow/package.json | 24 +++++ .../destinations/1flow/src/1flow.ts | 22 +++++ .../1flow/src/__tests__/index.test.ts | 14 +++ .../destinations/1flow/src/api.ts | 11 +++ .../destinations/1flow/src/generated-types.ts | 8 ++ .../src/identifyUser/__tests__/index.test.ts | 14 +++ .../1flow/src/identifyUser/generated-types.ts | 34 +++++++ .../1flow/src/identifyUser/index.ts | 90 +++++++++++++++++++ .../destinations/1flow/src/index.ts | 58 ++++++++++++ .../src/trackEvent/__tests__/index.test.ts | 14 +++ .../1flow/src/trackEvent/generated-types.ts | 22 +++++ .../1flow/src/trackEvent/index.ts | 59 ++++++++++++ .../destinations/1flow/tsconfig.json | 9 ++ 14 files changed, 410 insertions(+) create mode 100644 packages/browser-destinations/destinations/1flow/README.md create mode 100644 packages/browser-destinations/destinations/1flow/package.json create mode 100644 packages/browser-destinations/destinations/1flow/src/1flow.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/api.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/generated-types.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/index.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts create mode 100644 packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts create mode 100644 packages/browser-destinations/destinations/1flow/tsconfig.json diff --git a/packages/browser-destinations/destinations/1flow/README.md b/packages/browser-destinations/destinations/1flow/README.md new file mode 100644 index 0000000000..eb893c5ade --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/README.md @@ -0,0 +1,31 @@ +# @segment/analytics-browser-actions-1flow + +The 1Flow browser action destination for use with @segment/analytics-next. + +## License + +MIT License + +Copyright (c) 2023 Segment + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +## Contributing + +All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. diff --git a/packages/browser-destinations/destinations/1flow/package.json b/packages/browser-destinations/destinations/1flow/package.json new file mode 100644 index 0000000000..9a0217a5ad --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/package.json @@ -0,0 +1,24 @@ +{ + "name": "@segment/analytics-browser-actions-1flow", + "version": "1.0.0", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/segmentio/action-destinations", + "directory": "packages/browser-destinations/destinations/1flow" + }, + "main": "./dist/cjs", + "module": "./dist/esm", + "scripts": { + "build": "yarn build:esm && yarn build:cjs", + "build:cjs": "tsc --module commonjs --outDir ./dist/cjs", + "build:esm": "tsc --outDir ./dist/esm" + }, + "typings": "./dist/esm", + "dependencies": { + "@segment/browser-destination-runtime": "^1.4.0" + }, + "peerDependencies": { + "@segment/analytics-next": ">=1.55.0" + } +} diff --git a/packages/browser-destinations/destinations/1flow/src/1flow.ts b/packages/browser-destinations/destinations/1flow/src/1flow.ts new file mode 100644 index 0000000000..67bf6a48a2 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/1flow.ts @@ -0,0 +1,22 @@ +/* eslint-disable */ +// @ts-nocheck + +export function initScript({ projectApiKey }) { + //Set your APP_ID + const apiKey = projectApiKey + + const autoURLTracking = false + ;(function (w, o, s, t, k, a, r) { + ;(w._1flow = function (e, d, v) { + s(function () { + w._1flow(e, d, !v ? {} : v) + }, 5) + }), + (a = o.getElementsByTagName('head')[0]) + r = o.createElement('script') + r.async = 1 + r.setAttribute('data-api-key', k) + r.src = t + a.appendChild(r) + })(window, document, setTimeout, 'https://cdn-development.1flow.ai/js-sdk/1flow.js', apiKey) +} diff --git a/packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts new file mode 100644 index 0000000000..cd839e0ed5 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/__tests__/index.test.ts @@ -0,0 +1,14 @@ +import _1FlowDestination from '../index' +import { _1Flow } from '../api' + +describe('_1Flow', () => { + beforeAll(() => { + jest.mock('@segment/browser-destination-runtime/load-script', () => ({ + loadScript: (_src: any, _attributes: any) => {} + })) + jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({ + resolveWhen: (_fn: any, _timeout: any) => {} + })) + }) + test('it maps event parameters correctly to identify function ', async () => {}) +}) diff --git a/packages/browser-destinations/destinations/1flow/src/api.ts b/packages/browser-destinations/destinations/1flow/src/api.ts new file mode 100644 index 0000000000..b8279d0f4e --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/api.ts @@ -0,0 +1,11 @@ +type method = 'track' | 'identify' + +type _1FlowApi = { + richLinkProperties: string[] | undefined + activator: string | undefined + projectApiKey: string +} + +type _1FlowFunction = (method: method, ...args: unknown[]) => void + +export type _1Flow = _1FlowFunction & _1FlowApi diff --git a/packages/browser-destinations/destinations/1flow/src/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/generated-types.ts new file mode 100644 index 0000000000..ad85489880 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * This is the unique app_id for your 1Flow application, serving as the identifier for data storage and retrieval. This field is mandatory. + */ + projectApiKey: string +} diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts new file mode 100644 index 0000000000..f080cf0579 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/__tests__/index.test.ts @@ -0,0 +1,14 @@ +import _1FlowDestination from '../../index' + +describe('identify', () => { + beforeAll(() => { + jest.mock('@segment/browser-destination-runtime/load-script', () => ({ + loadScript: (_src: any, _attributes: any) => {} + })) + jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({ + resolveWhen: (_fn: any, _timeout: any) => {} + })) + }) + + test('it maps event parameters correctly to identify function ', async () => {}) +}) diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts new file mode 100644 index 0000000000..f5b1336f24 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/generated-types.ts @@ -0,0 +1,34 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for the user. + */ + userId?: string + /** + * An anonymous identifier for the user. + */ + anonymousId?: string + /** + * The user's custom attributes. + */ + traits?: { + [k: string]: unknown + } + /** + * The user's first name. + */ + first_name?: string + /** + * The user's last name. + */ + last_name?: string + /** + * The user's phone number. + */ + phone?: string + /** + * The user's email address. + */ + email?: string +} diff --git a/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts b/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts new file mode 100644 index 0000000000..80401e35a7 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/identifyUser/index.ts @@ -0,0 +1,90 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import { _1Flow } from '../api' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +const action: BrowserActionDefinition = { + title: 'Identify User', + description: 'Create or update a user in 1Flow.', + defaultSubscription: 'type = "identify"', + platform: 'web', + fields: { + userId: { + description: 'A unique identifier for the user.', + label: 'User ID', + type: 'string', + required: false, + default: { + '@path': '$.userId' + } + }, + anonymousId: { + description: 'An anonymous identifier for the user.', + label: 'Anonymous ID', + type: 'string', + required: false, + default: { + '@path': '$.anonymousId' + } + }, + traits: { + description: "The user's custom attributes.", + label: 'Custom Attributes', + type: 'object', + required: false, + defaultObjectUI: 'keyvalue', + default: { + '@path': '$.traits' + } + }, + first_name: { + description: "The user's first name.", + label: 'First Name', + type: 'string', + required: false, + default: { + '@path': '$.traits.first_name' + } + }, + last_name: { + description: "The user's last name.", + label: 'First Name', + type: 'string', + required: false, + default: { + '@path': '$.traits.last_name' + } + }, + phone: { + description: "The user's phone number.", + label: 'Phone Number', + type: 'string', + required: false, + default: { + '@path': '$.traits.phone' + } + }, + + email: { + description: "The user's email address.", + label: 'Email Address', + type: 'string', + required: false, + default: { + '@path': '$.traits.email' + } + } + }, + perform: (_1Flow, event) => { + const { userId, anonymousId, traits, first_name, last_name, phone, email } = event.payload + _1Flow('identify', userId, anonymousId, { + ...traits, + first_name: first_name, + last_name: last_name, + phone: phone, + email: email + }) + } +} + +export default action diff --git a/packages/browser-destinations/destinations/1flow/src/index.ts b/packages/browser-destinations/destinations/1flow/src/index.ts new file mode 100644 index 0000000000..aa98b2e2d0 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/index.ts @@ -0,0 +1,58 @@ +import type { Settings } from './generated-types' +import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types' +import { browserDestination } from '@segment/browser-destination-runtime/shim' +import trackEvent from './trackEvent' +import { initScript } from './1flow' +import { _1Flow } from './api' +import identifyUser from './identifyUser' +import { defaultValues } from '@segment/actions-core' +declare global { + interface Window { + _1Flow: _1Flow + } +} + +export const destination: BrowserDestinationDefinition = { + name: '1Flow', + slug: 'actions-1flow', + mode: 'device', + description: 'Send analytics from Segment to 1Flow', + settings: { + projectApiKey: { + description: + 'This is the unique app_id for your 1Flow application, serving as the identifier for data storage and retrieval. This field is mandatory.', + label: 'Project API Key', + type: 'string', + required: true + } + }, + presets: [ + { + name: 'Track Event', + subscribe: 'type = "track"', + partnerAction: 'trackEvent', + mapping: defaultValues(trackEvent.fields), + type: 'automatic' + }, + { + name: 'Identify User', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: defaultValues(identifyUser.fields), + type: 'automatic' + } + ], + + initialize: async ({ settings }, deps) => { + const projectApiKey = settings.projectApiKey + initScript({ projectApiKey }) + await deps.resolveWhen(() => Object.prototype.hasOwnProperty.call(window, '_1Flow'), 100) + return window._1Flow + }, + actions: { + trackEvent, + identifyUser + } +} + +export default browserDestination(destination) diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..8564c31fea --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/__tests__/index.test.ts @@ -0,0 +1,14 @@ +import _1flowDestination from '../../index' + +describe('track', () => { + beforeAll(() => { + jest.mock('@segment/browser-destination-runtime/load-script', () => ({ + loadScript: (_src: any, _attributes: any) => {} + })) + jest.mock('@segment/browser-destination-runtime/resolve-when', () => ({ + resolveWhen: (_fn: any, _timeout: any) => {} + })) + }) + + test('it maps event parameters correctly to track function', async () => {}) +}) diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts new file mode 100644 index 0000000000..5c7fd1c746 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/generated-types.ts @@ -0,0 +1,22 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The name of the event. + */ + event_name: string + /** + * A unique identifier for the user. + */ + userId?: string + /** + * An anonymous identifier for the user. + */ + anonymousId?: string + /** + * Information associated with the event + */ + properties?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts b/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts new file mode 100644 index 0000000000..0fec9ca043 --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/src/trackEvent/index.ts @@ -0,0 +1,59 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import { _1Flow } from '../api' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +const action: BrowserActionDefinition = { + title: 'Track Event', + description: 'Submit an event to 1Flow.', + defaultSubscription: 'type = "track"', + platform: 'web', + fields: { + event_name: { + description: 'The name of the event.', + label: 'Event Name', + type: 'string', + required: true, + default: { + '@path': '$.event' + } + }, + userId: { + description: 'A unique identifier for the user.', + label: 'User ID', + type: 'string', + required: false, + default: { + '@path': '$.userId' + } + }, + anonymousId: { + description: 'An anonymous identifier for the user.', + label: 'Anonymous ID', + type: 'string', + required: false, + default: { + '@path': '$.anonymousId' + } + }, + properties: { + description: 'Information associated with the event', + label: 'Event Properties', + type: 'object', + required: false, + default: { + '@path': '$.properties' + } + } + }, + perform: (_1Flow, event) => { + const { event_name, userId, anonymousId, properties } = event.payload + _1Flow('track', event_name, { + userId: userId, + anonymousId: anonymousId, + properties: properties + }) + } +} + +export default action diff --git a/packages/browser-destinations/destinations/1flow/tsconfig.json b/packages/browser-destinations/destinations/1flow/tsconfig.json new file mode 100644 index 0000000000..c2a7897afd --- /dev/null +++ b/packages/browser-destinations/destinations/1flow/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "baseUrl": "." + }, + "include": ["src"], + "exclude": ["dist", "**/__tests__"] +} From 751960cc8912b81f900a9e1c2e3993d38aca763d Mon Sep 17 00:00:00 2001 From: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:04:53 +0000 Subject: [PATCH 14/34] Publish - @segment/actions-shared@1.71.0 - @segment/browser-destination-runtime@1.20.0 - @segment/actions-core@3.90.0 - @segment/action-destinations@3.230.0 - @segment/destinations-manifest@1.29.0 - @segment/analytics-browser-actions-1flow@1.1.0 - @segment/analytics-browser-actions-adobe-target@1.21.0 - @segment/analytics-browser-actions-amplitude-plugins@1.21.0 - @segment/analytics-browser-actions-braze-cloud-plugins@1.24.0 - @segment/analytics-browser-actions-braze@1.24.0 - @segment/analytics-browser-actions-cdpresolution@1.8.0 - @segment/analytics-browser-actions-commandbar@1.21.0 - @segment/analytics-browser-actions-devrev@1.8.0 - @segment/analytics-browser-actions-friendbuy@1.21.0 - @segment/analytics-browser-actions-fullstory@1.22.0 - @segment/analytics-browser-actions-google-analytics-4@1.25.0 - @segment/analytics-browser-actions-google-campaign-manager@1.11.0 - @segment/analytics-browser-actions-heap@1.21.0 - @segment/analytics-browser-hubble-web@1.7.0 - @segment/analytics-browser-actions-hubspot@1.21.0 - @segment/analytics-browser-actions-intercom@1.21.0 - @segment/analytics-browser-actions-iterate@1.21.0 - @segment/analytics-browser-actions-jimo@1.7.0 - @segment/analytics-browser-actions-koala@1.21.0 - @segment/analytics-browser-actions-logrocket@1.21.0 - @segment/analytics-browser-actions-pendo-web-actions@1.9.0 - @segment/analytics-browser-actions-playerzero@1.21.0 - @segment/analytics-browser-actions-replaybird@1.2.0 - @segment/analytics-browser-actions-ripe@1.21.0 - @segment/analytics-browser-actions-rupt@1.10.0 - @segment/analytics-browser-actions-screeb@1.21.0 - @segment/analytics-browser-actions-utils@1.21.0 - @segment/analytics-browser-actions-snap-plugins@1.2.0 - @segment/analytics-browser-actions-sprig@1.21.0 - @segment/analytics-browser-actions-stackadapt@1.21.0 - @segment/analytics-browser-actions-tiktok-pixel@1.18.0 - @segment/analytics-browser-actions-upollo@1.21.0 - @segment/analytics-browser-actions-userpilot@1.21.0 - @segment/analytics-browser-actions-vwo@1.22.0 - @segment/analytics-browser-actions-wiseops@1.21.0 --- packages/actions-shared/package.json | 4 +- .../browser-destination-runtime/package.json | 4 +- .../destinations/1flow/package.json | 4 +- .../destinations/adobe-target/package.json | 4 +- .../amplitude-plugins/package.json | 4 +- .../braze-cloud-plugins/package.json | 6 +- .../destinations/braze/package.json | 6 +- .../destinations/cdpresolution/package.json | 4 +- .../destinations/commandbar/package.json | 6 +- .../destinations/devrev/package.json | 4 +- .../destinations/friendbuy/package.json | 8 +-- .../destinations/fullstory/package.json | 6 +- .../google-analytics-4-web/package.json | 6 +- .../google-campaign-manager/package.json | 4 +- .../destinations/heap/package.json | 6 +- .../destinations/hubble-web/package.json | 6 +- .../destinations/hubspot-web/package.json | 6 +- .../destinations/intercom/package.json | 8 +-- .../destinations/iterate/package.json | 6 +- .../destinations/jimo/package.json | 4 +- .../destinations/koala/package.json | 6 +- .../destinations/logrocket/package.json | 6 +- .../pendo-web-actions/package.json | 4 +- .../destinations/playerzero-web/package.json | 6 +- .../destinations/replaybird/package.json | 4 +- .../destinations/ripe/package.json | 6 +- .../destinations/rupt/package.json | 6 +- .../destinations/screeb/package.json | 6 +- .../segment-utilities-web/package.json | 4 +- .../destinations/snap-plugins/package.json | 4 +- .../destinations/sprig-web/package.json | 6 +- .../destinations/stackadapt/package.json | 6 +- .../destinations/tiktok-pixel/package.json | 6 +- .../destinations/upollo/package.json | 6 +- .../destinations/userpilot/package.json | 6 +- .../destinations/vwo/package.json | 6 +- .../destinations/wisepops/package.json | 6 +- packages/core/package.json | 2 +- packages/destination-actions/package.json | 6 +- packages/destinations-manifest/package.json | 68 +++++++++---------- 40 files changed, 138 insertions(+), 138 deletions(-) diff --git a/packages/actions-shared/package.json b/packages/actions-shared/package.json index 37df6a510c..8bbb55e5de 100644 --- a/packages/actions-shared/package.json +++ b/packages/actions-shared/package.json @@ -1,7 +1,7 @@ { "name": "@segment/actions-shared", "description": "Shared destination action methods and definitions.", - "version": "1.70.0", + "version": "1.71.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -37,7 +37,7 @@ }, "dependencies": { "@amplitude/ua-parser-js": "^0.7.25", - "@segment/actions-core": "^3.88.0", + "@segment/actions-core": "^3.90.0", "cheerio": "^1.0.0-rc.10", "dayjs": "^1.10.7", "escape-goat": "^3", diff --git a/packages/browser-destination-runtime/package.json b/packages/browser-destination-runtime/package.json index f9281dbf52..4ed0779ba4 100644 --- a/packages/browser-destination-runtime/package.json +++ b/packages/browser-destination-runtime/package.json @@ -1,6 +1,6 @@ { "name": "@segment/browser-destination-runtime", - "version": "1.19.0", + "version": "1.20.0", "license": "MIT", "publishConfig": { "access": "public", @@ -62,7 +62,7 @@ } }, "dependencies": { - "@segment/actions-core": "^3.88.0" + "@segment/actions-core": "^3.90.0" }, "devDependencies": { "@segment/analytics-next": "*" diff --git a/packages/browser-destinations/destinations/1flow/package.json b/packages/browser-destinations/destinations/1flow/package.json index 9a0217a5ad..116dba3b5c 100644 --- a/packages/browser-destinations/destinations/1flow/package.json +++ b/packages/browser-destinations/destinations/1flow/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-1flow", - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "repository": { "type": "git", @@ -16,7 +16,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.4.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/adobe-target/package.json b/packages/browser-destinations/destinations/adobe-target/package.json index 96c3ef8eca..c0bf813aaf 100644 --- a/packages/browser-destinations/destinations/adobe-target/package.json +++ b/packages/browser-destinations/destinations/adobe-target/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-adobe-target", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -16,7 +16,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/amplitude-plugins/package.json b/packages/browser-destinations/destinations/amplitude-plugins/package.json index c95bdb8fc9..c8ce4bae3f 100644 --- a/packages/browser-destinations/destinations/amplitude-plugins/package.json +++ b/packages/browser-destinations/destinations/amplitude-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-amplitude-plugins", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/braze-cloud-plugins/package.json b/packages/browser-destinations/destinations/braze-cloud-plugins/package.json index 26b8061156..ebc9800a62 100644 --- a/packages/browser-destinations/destinations/braze-cloud-plugins/package.json +++ b/packages/browser-destinations/destinations/braze-cloud-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-braze-cloud-plugins", - "version": "1.23.0", + "version": "1.24.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/analytics-browser-actions-braze": "^1.23.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/analytics-browser-actions-braze": "^1.24.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/braze/package.json b/packages/browser-destinations/destinations/braze/package.json index 727ddc9027..1927085ed2 100644 --- a/packages/browser-destinations/destinations/braze/package.json +++ b/packages/browser-destinations/destinations/braze/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-braze", - "version": "1.23.0", + "version": "1.24.0", "license": "MIT", "publishConfig": { "access": "public", @@ -35,8 +35,8 @@ "dependencies": { "@braze/web-sdk": "npm:@braze/web-sdk@^4.1.0", "@braze/web-sdk-v3": "npm:@braze/web-sdk@^3.5.1", - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/cdpresolution/package.json b/packages/browser-destinations/destinations/cdpresolution/package.json index aba4618cdf..f729d428bf 100644 --- a/packages/browser-destinations/destinations/cdpresolution/package.json +++ b/packages/browser-destinations/destinations/cdpresolution/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-cdpresolution", - "version": "1.7.0", + "version": "1.8.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/commandbar/package.json b/packages/browser-destinations/destinations/commandbar/package.json index 7c5682241c..2510649c40 100644 --- a/packages/browser-destinations/destinations/commandbar/package.json +++ b/packages/browser-destinations/destinations/commandbar/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-commandbar", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/devrev/package.json b/packages/browser-destinations/destinations/devrev/package.json index 29c1b42642..a1fd263c46 100644 --- a/packages/browser-destinations/destinations/devrev/package.json +++ b/packages/browser-destinations/destinations/devrev/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-devrev", - "version": "1.7.0", + "version": "1.8.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/friendbuy/package.json b/packages/browser-destinations/destinations/friendbuy/package.json index 90f41153c1..d67b7e1d42 100644 --- a/packages/browser-destinations/destinations/friendbuy/package.json +++ b/packages/browser-destinations/destinations/friendbuy/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-friendbuy", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,9 +15,9 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/actions-shared": "^1.70.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/actions-shared": "^1.71.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/fullstory/package.json b/packages/browser-destinations/destinations/fullstory/package.json index c41d0c7516..7b78395fc1 100644 --- a/packages/browser-destinations/destinations/fullstory/package.json +++ b/packages/browser-destinations/destinations/fullstory/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-fullstory", - "version": "1.21.0", + "version": "1.22.0", "license": "MIT", "publishConfig": { "access": "public", @@ -16,8 +16,8 @@ "typings": "./dist/esm", "dependencies": { "@fullstory/browser": "^1.4.9", - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/google-analytics-4-web/package.json b/packages/browser-destinations/destinations/google-analytics-4-web/package.json index 2b74d0c3fa..88e5566464 100644 --- a/packages/browser-destinations/destinations/google-analytics-4-web/package.json +++ b/packages/browser-destinations/destinations/google-analytics-4-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-google-analytics-4", - "version": "1.24.0", + "version": "1.25.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/google-campaign-manager/package.json b/packages/browser-destinations/destinations/google-campaign-manager/package.json index 1ea2a4e7a4..5bcbe4380a 100644 --- a/packages/browser-destinations/destinations/google-campaign-manager/package.json +++ b/packages/browser-destinations/destinations/google-campaign-manager/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-google-campaign-manager", - "version": "1.10.0", + "version": "1.11.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/heap/package.json b/packages/browser-destinations/destinations/heap/package.json index a475bb2f7f..50822569bc 100644 --- a/packages/browser-destinations/destinations/heap/package.json +++ b/packages/browser-destinations/destinations/heap/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-heap", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/hubble-web/package.json b/packages/browser-destinations/destinations/hubble-web/package.json index 6f777e3ede..2c481773c9 100644 --- a/packages/browser-destinations/destinations/hubble-web/package.json +++ b/packages/browser-destinations/destinations/hubble-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-hubble-web", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/hubspot-web/package.json b/packages/browser-destinations/destinations/hubspot-web/package.json index 09d01316c2..a349b6e478 100644 --- a/packages/browser-destinations/destinations/hubspot-web/package.json +++ b/packages/browser-destinations/destinations/hubspot-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-hubspot", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/intercom/package.json b/packages/browser-destinations/destinations/intercom/package.json index 335dc7b5a1..6a3d184535 100644 --- a/packages/browser-destinations/destinations/intercom/package.json +++ b/packages/browser-destinations/destinations/intercom/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-intercom", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,9 +15,9 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/actions-shared": "^1.70.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/actions-shared": "^1.71.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/iterate/package.json b/packages/browser-destinations/destinations/iterate/package.json index 8ce481fdb5..1f6a0da30f 100644 --- a/packages/browser-destinations/destinations/iterate/package.json +++ b/packages/browser-destinations/destinations/iterate/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-iterate", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/jimo/package.json b/packages/browser-destinations/destinations/jimo/package.json index f90539a468..35a896e2df 100644 --- a/packages/browser-destinations/destinations/jimo/package.json +++ b/packages/browser-destinations/destinations/jimo/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-jimo", - "version": "1.6.0", + "version": "1.7.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/koala/package.json b/packages/browser-destinations/destinations/koala/package.json index 9abb083614..4ee1c3300b 100644 --- a/packages/browser-destinations/destinations/koala/package.json +++ b/packages/browser-destinations/destinations/koala/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-koala", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/logrocket/package.json b/packages/browser-destinations/destinations/logrocket/package.json index 3c9bf1e041..e45d964d73 100644 --- a/packages/browser-destinations/destinations/logrocket/package.json +++ b/packages/browser-destinations/destinations/logrocket/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-logrocket", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0", + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0", "logrocket": "^3.0.1" }, "peerDependencies": { diff --git a/packages/browser-destinations/destinations/pendo-web-actions/package.json b/packages/browser-destinations/destinations/pendo-web-actions/package.json index 0392d18b99..dc7e9caaa9 100644 --- a/packages/browser-destinations/destinations/pendo-web-actions/package.json +++ b/packages/browser-destinations/destinations/pendo-web-actions/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-pendo-web-actions", - "version": "1.8.0", + "version": "1.9.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/playerzero-web/package.json b/packages/browser-destinations/destinations/playerzero-web/package.json index 365faee91d..7a8f8a5086 100644 --- a/packages/browser-destinations/destinations/playerzero-web/package.json +++ b/packages/browser-destinations/destinations/playerzero-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-playerzero", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/replaybird/package.json b/packages/browser-destinations/destinations/replaybird/package.json index 16117ce196..074957e6fc 100644 --- a/packages/browser-destinations/destinations/replaybird/package.json +++ b/packages/browser-destinations/destinations/replaybird/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-replaybird", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/ripe/package.json b/packages/browser-destinations/destinations/ripe/package.json index c832ec8388..3325ae5a1e 100644 --- a/packages/browser-destinations/destinations/ripe/package.json +++ b/packages/browser-destinations/destinations/ripe/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-ripe", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/rupt/package.json b/packages/browser-destinations/destinations/rupt/package.json index 88da9fe2a8..1e9874f307 100644 --- a/packages/browser-destinations/destinations/rupt/package.json +++ b/packages/browser-destinations/destinations/rupt/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-rupt", - "version": "1.9.0", + "version": "1.10.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/screeb/package.json b/packages/browser-destinations/destinations/screeb/package.json index 3d2d35095e..05d4673f75 100644 --- a/packages/browser-destinations/destinations/screeb/package.json +++ b/packages/browser-destinations/destinations/screeb/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-screeb", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/segment-utilities-web/package.json b/packages/browser-destinations/destinations/segment-utilities-web/package.json index 5a7cae6a0b..d23327099a 100644 --- a/packages/browser-destinations/destinations/segment-utilities-web/package.json +++ b/packages/browser-destinations/destinations/segment-utilities-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-utils", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/snap-plugins/package.json b/packages/browser-destinations/destinations/snap-plugins/package.json index bad3d84473..85502c497c 100644 --- a/packages/browser-destinations/destinations/snap-plugins/package.json +++ b/packages/browser-destinations/destinations/snap-plugins/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-snap-plugins", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/sprig-web/package.json b/packages/browser-destinations/destinations/sprig-web/package.json index 335ff70204..3447a47da6 100644 --- a/packages/browser-destinations/destinations/sprig-web/package.json +++ b/packages/browser-destinations/destinations/sprig-web/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-sprig", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/stackadapt/package.json b/packages/browser-destinations/destinations/stackadapt/package.json index 5cac1e32f8..96fef383b9 100644 --- a/packages/browser-destinations/destinations/stackadapt/package.json +++ b/packages/browser-destinations/destinations/stackadapt/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-stackadapt", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/tiktok-pixel/package.json b/packages/browser-destinations/destinations/tiktok-pixel/package.json index b6de255c20..e6c25fe35b 100644 --- a/packages/browser-destinations/destinations/tiktok-pixel/package.json +++ b/packages/browser-destinations/destinations/tiktok-pixel/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-tiktok-pixel", - "version": "1.17.0", + "version": "1.18.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/upollo/package.json b/packages/browser-destinations/destinations/upollo/package.json index 9ef8e9ec7e..37c3d6c9ae 100644 --- a/packages/browser-destinations/destinations/upollo/package.json +++ b/packages/browser-destinations/destinations/upollo/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-upollo", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/userpilot/package.json b/packages/browser-destinations/destinations/userpilot/package.json index fbd6c6e8f1..2d658fa5ed 100644 --- a/packages/browser-destinations/destinations/userpilot/package.json +++ b/packages/browser-destinations/destinations/userpilot/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-userpilot", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/vwo/package.json b/packages/browser-destinations/destinations/vwo/package.json index 6e90ff3186..a008370fdc 100644 --- a/packages/browser-destinations/destinations/vwo/package.json +++ b/packages/browser-destinations/destinations/vwo/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-vwo", - "version": "1.21.0", + "version": "1.22.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/browser-destinations/destinations/wisepops/package.json b/packages/browser-destinations/destinations/wisepops/package.json index 01433814cf..c3559f21f6 100644 --- a/packages/browser-destinations/destinations/wisepops/package.json +++ b/packages/browser-destinations/destinations/wisepops/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-wiseops", - "version": "1.20.0", + "version": "1.21.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,8 +15,8 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/actions-core": "^3.88.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" }, "peerDependencies": { "@segment/analytics-next": ">=1.55.0" diff --git a/packages/core/package.json b/packages/core/package.json index 3d8255a371..ab23d06912 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@segment/actions-core", "description": "Core runtime for Destinations Actions.", - "version": "3.89.0", + "version": "3.90.0", "repository": { "type": "git", "url": "https://github.com/segmentio/fab-5-engine", diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json index 691775d92a..f636727eb0 100644 --- a/packages/destination-actions/package.json +++ b/packages/destination-actions/package.json @@ -1,7 +1,7 @@ { "name": "@segment/action-destinations", "description": "Destination Actions engine and definitions.", - "version": "3.229.0", + "version": "3.230.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -43,8 +43,8 @@ "@bufbuild/protobuf": "^1.4.2", "@bufbuild/protoc-gen-es": "^1.4.2", "@segment/a1-notation": "^2.1.4", - "@segment/actions-core": "^3.88.0", - "@segment/actions-shared": "^1.70.0", + "@segment/actions-core": "^3.90.0", + "@segment/actions-shared": "^1.71.0", "@types/node": "^18.11.15", "ajv-formats": "^2.1.1", "aws4": "^1.12.0", diff --git a/packages/destinations-manifest/package.json b/packages/destinations-manifest/package.json index e571436af3..64327e5420 100644 --- a/packages/destinations-manifest/package.json +++ b/packages/destinations-manifest/package.json @@ -1,6 +1,6 @@ { "name": "@segment/destinations-manifest", - "version": "1.28.0", + "version": "1.29.0", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" @@ -12,39 +12,39 @@ "main": "./dist/index.js", "typings": "./dist/index.d.ts", "dependencies": { - "@segment/analytics-browser-actions-adobe-target": "^1.20.0", - "@segment/analytics-browser-actions-amplitude-plugins": "^1.20.0", - "@segment/analytics-browser-actions-braze": "^1.23.0", - "@segment/analytics-browser-actions-braze-cloud-plugins": "^1.23.0", - "@segment/analytics-browser-actions-cdpresolution": "^1.7.0", - "@segment/analytics-browser-actions-commandbar": "^1.20.0", - "@segment/analytics-browser-actions-devrev": "^1.7.0", - "@segment/analytics-browser-actions-friendbuy": "^1.20.0", - "@segment/analytics-browser-actions-fullstory": "^1.21.0", - "@segment/analytics-browser-actions-google-analytics-4": "^1.24.0", - "@segment/analytics-browser-actions-google-campaign-manager": "^1.10.0", - "@segment/analytics-browser-actions-heap": "^1.20.0", - "@segment/analytics-browser-actions-hubspot": "^1.20.0", - "@segment/analytics-browser-actions-intercom": "^1.20.0", - "@segment/analytics-browser-actions-iterate": "^1.20.0", - "@segment/analytics-browser-actions-koala": "^1.20.0", - "@segment/analytics-browser-actions-logrocket": "^1.20.0", - "@segment/analytics-browser-actions-pendo-web-actions": "^1.8.0", - "@segment/analytics-browser-actions-playerzero": "^1.20.0", - "@segment/analytics-browser-actions-ripe": "^1.20.0", + "@segment/analytics-browser-actions-adobe-target": "^1.21.0", + "@segment/analytics-browser-actions-amplitude-plugins": "^1.21.0", + "@segment/analytics-browser-actions-braze": "^1.24.0", + "@segment/analytics-browser-actions-braze-cloud-plugins": "^1.24.0", + "@segment/analytics-browser-actions-cdpresolution": "^1.8.0", + "@segment/analytics-browser-actions-commandbar": "^1.21.0", + "@segment/analytics-browser-actions-devrev": "^1.8.0", + "@segment/analytics-browser-actions-friendbuy": "^1.21.0", + "@segment/analytics-browser-actions-fullstory": "^1.22.0", + "@segment/analytics-browser-actions-google-analytics-4": "^1.25.0", + "@segment/analytics-browser-actions-google-campaign-manager": "^1.11.0", + "@segment/analytics-browser-actions-heap": "^1.21.0", + "@segment/analytics-browser-actions-hubspot": "^1.21.0", + "@segment/analytics-browser-actions-intercom": "^1.21.0", + "@segment/analytics-browser-actions-iterate": "^1.21.0", + "@segment/analytics-browser-actions-koala": "^1.21.0", + "@segment/analytics-browser-actions-logrocket": "^1.21.0", + "@segment/analytics-browser-actions-pendo-web-actions": "^1.9.0", + "@segment/analytics-browser-actions-playerzero": "^1.21.0", + "@segment/analytics-browser-actions-replaybird": "^1.2.0", + "@segment/analytics-browser-actions-ripe": "^1.21.0", "@segment/analytics-browser-actions-sabil": "^1.6.0", - "@segment/analytics-browser-actions-screeb": "^1.20.0", - "@segment/analytics-browser-actions-sprig": "^1.20.0", - "@segment/analytics-browser-actions-stackadapt": "^1.20.0", - "@segment/analytics-browser-actions-tiktok-pixel": "^1.17.0", - "@segment/analytics-browser-actions-upollo": "^1.20.0", - "@segment/analytics-browser-actions-userpilot": "^1.20.0", - "@segment/analytics-browser-actions-utils": "^1.20.0", - "@segment/analytics-browser-actions-vwo": "^1.21.0", - "@segment/analytics-browser-actions-wiseops": "^1.20.0", - "@segment/analytics-browser-hubble-web": "^1.6.0", - "@segment/analytics-browser-actions-snap-plugins": "^1.1.0", - "@segment/analytics-browser-actions-replaybird": "^1.1.0", - "@segment/browser-destination-runtime": "^1.19.0" + "@segment/analytics-browser-actions-screeb": "^1.21.0", + "@segment/analytics-browser-actions-snap-plugins": "^1.2.0", + "@segment/analytics-browser-actions-sprig": "^1.21.0", + "@segment/analytics-browser-actions-stackadapt": "^1.21.0", + "@segment/analytics-browser-actions-tiktok-pixel": "^1.18.0", + "@segment/analytics-browser-actions-upollo": "^1.21.0", + "@segment/analytics-browser-actions-userpilot": "^1.21.0", + "@segment/analytics-browser-actions-utils": "^1.21.0", + "@segment/analytics-browser-actions-vwo": "^1.22.0", + "@segment/analytics-browser-actions-wiseops": "^1.21.0", + "@segment/analytics-browser-hubble-web": "^1.7.0", + "@segment/browser-destination-runtime": "^1.20.0" } } From d61bc35113d6b5a5b37fb751702241ce82e37e4a Mon Sep 17 00:00:00 2001 From: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:11:53 +0000 Subject: [PATCH 15/34] registering 1flow and jimo web Integrations --- .../browser-destinations/destinations/1flow/package.json | 7 +++---- packages/destinations-manifest/package.json | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/browser-destinations/destinations/1flow/package.json b/packages/browser-destinations/destinations/1flow/package.json index 116dba3b5c..de1558923b 100644 --- a/packages/browser-destinations/destinations/1flow/package.json +++ b/packages/browser-destinations/destinations/1flow/package.json @@ -2,10 +2,9 @@ "name": "@segment/analytics-browser-actions-1flow", "version": "1.1.0", "license": "MIT", - "repository": { - "type": "git", - "url": "https://github.com/segmentio/action-destinations", - "directory": "packages/browser-destinations/destinations/1flow" + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" }, "main": "./dist/cjs", "module": "./dist/esm", diff --git a/packages/destinations-manifest/package.json b/packages/destinations-manifest/package.json index 64327e5420..13a83ab0a9 100644 --- a/packages/destinations-manifest/package.json +++ b/packages/destinations-manifest/package.json @@ -45,6 +45,8 @@ "@segment/analytics-browser-actions-vwo": "^1.22.0", "@segment/analytics-browser-actions-wiseops": "^1.21.0", "@segment/analytics-browser-hubble-web": "^1.7.0", + "@segment/analytics-browser-actions-jimo": "^1.7.0", + "@segment/analytics-browser-actions-1flow": "^1.1.0", "@segment/browser-destination-runtime": "^1.20.0" } } From 3f163ca4347d93c3547053b601026afac96e0c90 Mon Sep 17 00:00:00 2001 From: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:56:42 +0000 Subject: [PATCH 16/34] attempting to fix breaking buildkite CI --- .../src/destinations/display-video-360/errors.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/destination-actions/src/destinations/display-video-360/errors.ts b/packages/destination-actions/src/destinations/display-video-360/errors.ts index 082949cfe1..bb5e72dff9 100644 --- a/packages/destination-actions/src/destinations/display-video-360/errors.ts +++ b/packages/destination-actions/src/destinations/display-video-360/errors.ts @@ -1,5 +1,4 @@ -import { ErrorCodes, IntegrationError } from '@segment/actions-core' -import { InvalidAuthenticationError } from '@segment/actions-core/*' +import { ErrorCodes, IntegrationError, InvalidAuthenticationError } from '@segment/actions-core' import { GoogleAPIError } from './types' From ccb94292064ba9079d192f8b5c3f3284073a1049 Mon Sep 17 00:00:00 2001 From: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 13:59:23 +0000 Subject: [PATCH 17/34] Publish - @segment/action-destinations@3.231.0 - @segment/destinations-manifest@1.30.0 - @segment/analytics-browser-actions-1flow@1.2.0 --- .../browser-destinations/destinations/1flow/package.json | 2 +- packages/destination-actions/package.json | 2 +- packages/destinations-manifest/package.json | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/browser-destinations/destinations/1flow/package.json b/packages/browser-destinations/destinations/1flow/package.json index de1558923b..787ad3cc69 100644 --- a/packages/browser-destinations/destinations/1flow/package.json +++ b/packages/browser-destinations/destinations/1flow/package.json @@ -1,6 +1,6 @@ { "name": "@segment/analytics-browser-actions-1flow", - "version": "1.1.0", + "version": "1.2.0", "license": "MIT", "publishConfig": { "access": "public", diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json index f636727eb0..529ca20c10 100644 --- a/packages/destination-actions/package.json +++ b/packages/destination-actions/package.json @@ -1,7 +1,7 @@ { "name": "@segment/action-destinations", "description": "Destination Actions engine and definitions.", - "version": "3.230.0", + "version": "3.231.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", diff --git a/packages/destinations-manifest/package.json b/packages/destinations-manifest/package.json index 13a83ab0a9..953a5bf817 100644 --- a/packages/destinations-manifest/package.json +++ b/packages/destinations-manifest/package.json @@ -1,6 +1,6 @@ { "name": "@segment/destinations-manifest", - "version": "1.29.0", + "version": "1.30.0", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" @@ -12,6 +12,7 @@ "main": "./dist/index.js", "typings": "./dist/index.d.ts", "dependencies": { + "@segment/analytics-browser-actions-1flow": "^1.2.0", "@segment/analytics-browser-actions-adobe-target": "^1.21.0", "@segment/analytics-browser-actions-amplitude-plugins": "^1.21.0", "@segment/analytics-browser-actions-braze": "^1.24.0", @@ -27,6 +28,7 @@ "@segment/analytics-browser-actions-hubspot": "^1.21.0", "@segment/analytics-browser-actions-intercom": "^1.21.0", "@segment/analytics-browser-actions-iterate": "^1.21.0", + "@segment/analytics-browser-actions-jimo": "^1.7.0", "@segment/analytics-browser-actions-koala": "^1.21.0", "@segment/analytics-browser-actions-logrocket": "^1.21.0", "@segment/analytics-browser-actions-pendo-web-actions": "^1.9.0", @@ -45,8 +47,6 @@ "@segment/analytics-browser-actions-vwo": "^1.22.0", "@segment/analytics-browser-actions-wiseops": "^1.21.0", "@segment/analytics-browser-hubble-web": "^1.7.0", - "@segment/analytics-browser-actions-jimo": "^1.7.0", - "@segment/analytics-browser-actions-1flow": "^1.1.0", "@segment/browser-destination-runtime": "^1.20.0" } } From c1b07c050e9162333bc743f40e31faa7d3cb6be7 Mon Sep 17 00:00:00 2001 From: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:21:54 +0100 Subject: [PATCH 18/34] Renaming 1Flow due to name clash --- packages/browser-destinations/destinations/1flow/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-destinations/destinations/1flow/src/index.ts b/packages/browser-destinations/destinations/1flow/src/index.ts index aa98b2e2d0..59edc58318 100644 --- a/packages/browser-destinations/destinations/1flow/src/index.ts +++ b/packages/browser-destinations/destinations/1flow/src/index.ts @@ -13,7 +13,7 @@ declare global { } export const destination: BrowserDestinationDefinition = { - name: '1Flow', + name: '1Flow Web (Actions)', slug: 'actions-1flow', mode: 'device', description: 'Send analytics from Segment to 1Flow', From ebd058258030ba5265639754cd0b9c25caf27231 Mon Sep 17 00:00:00 2001 From: joe-ayoub-segment <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:35:27 +0000 Subject: [PATCH 19/34] registering 1flow web actions --- packages/destinations-manifest/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/destinations-manifest/src/index.ts b/packages/destinations-manifest/src/index.ts index 1b874d0248..476fc9b2e8 100644 --- a/packages/destinations-manifest/src/index.ts +++ b/packages/destinations-manifest/src/index.ts @@ -63,3 +63,4 @@ register('651aac880f2c3b5a8736e0cc', '@segment/analytics-browser-hubble-web') register('652d4cf5e00c0147e6eaf5e7', '@segment/analytics-browser-actions-jimo') register('6261a8b6cb4caa70e19116e8', '@segment/analytics-browser-actions-snap-plugins') register('6554e468e280fb14fbb4433c', '@segment/analytics-browser-actions-replaybird') +register('656773f0bd79a3676ab2733d', '@segment/analytics-browser-actions-1flow') From c9dad648d1f5fb1bca1ab98e00858c24fb45291e Mon Sep 17 00:00:00 2001 From: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Date: Thu, 30 Nov 2023 13:40:03 +0530 Subject: [PATCH 20/34] [Strat-2879] | Set up DataDog Dashboard to track requests to Linkedin-audiences Actions (#1619) * [Strat-2879] | Set up DataDog Dashboard to track requests to Linkedin-audiences Actions. * rebuild * rebuild --------- Co-authored-by: Gaurav Kochar --- .../updateAudience/index.ts | 43 +++++++++++++------ 1 file changed, 31 insertions(+), 12 deletions(-) diff --git a/packages/destination-actions/src/destinations/linkedin-audiences/updateAudience/index.ts b/packages/destination-actions/src/destinations/linkedin-audiences/updateAudience/index.ts index 6648981d97..6917684cc2 100644 --- a/packages/destination-actions/src/destinations/linkedin-audiences/updateAudience/index.ts +++ b/packages/destination-actions/src/destinations/linkedin-audiences/updateAudience/index.ts @@ -1,4 +1,4 @@ -import type { ActionDefinition } from '@segment/actions-core' +import type { ActionDefinition, StatsContext } from '@segment/actions-core' import { RequestClient, RetryableError, IntegrationError } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' @@ -81,20 +81,25 @@ const action: ActionDefinition = { default: 'AUTO' } }, - perform: async (request, { settings, payload }) => { - return processPayload(request, settings, [payload]) + perform: async (request, { settings, payload, statsContext }) => { + return processPayload(request, settings, [payload], statsContext) }, - performBatch: async (request, { settings, payload }) => { - return processPayload(request, settings, payload) + performBatch: async (request, { settings, payload, statsContext }) => { + return processPayload(request, settings, payload, statsContext) } } -async function processPayload(request: RequestClient, settings: Settings, payloads: Payload[]) { +async function processPayload( + request: RequestClient, + settings: Settings, + payloads: Payload[], + statsContext: StatsContext | undefined +) { validate(settings, payloads) const linkedinApiClient: LinkedInAudiences = new LinkedInAudiences(request) - const dmpSegmentId = await getDmpSegmentId(linkedinApiClient, settings, payloads[0]) + const dmpSegmentId = await getDmpSegmentId(linkedinApiClient, settings, payloads[0], statsContext) const elements = extractUsers(settings, payloads) // We should never hit this condition because at least an email or a @@ -106,7 +111,10 @@ async function processPayload(request: RequestClient, settings: Settings, payloa if (elements.length < 1) { return } - + statsContext?.statsClient?.incr('oauth_app_api_call', 1, [ + ...statsContext?.tags, + `endpoint:add-or-remove-users-from-dmpSegment` + ]) const res = await linkedinApiClient.batchUpdate(dmpSegmentId, elements) // At this point, if LinkedIn's API returns a 404 error, it's because the audience @@ -139,18 +147,29 @@ function validate(settings: Settings, payloads: Payload[]): void { } } -async function getDmpSegmentId(linkedinApiClient: LinkedInAudiences, settings: Settings, payload: Payload) { +async function getDmpSegmentId( + linkedinApiClient: LinkedInAudiences, + settings: Settings, + payload: Payload, + statsContext: StatsContext | undefined +) { + statsContext?.statsClient?.incr('oauth_app_api_call', 1, [...statsContext?.tags, `endpoint:get-dmpSegment`]) const res = await linkedinApiClient.getDmpSegment(settings, payload) const body = await res.json() if (body.elements?.length > 0) { return body.elements[0].id } - - return createDmpSegment(linkedinApiClient, settings, payload) + return createDmpSegment(linkedinApiClient, settings, payload, statsContext) } -async function createDmpSegment(linkedinApiClient: LinkedInAudiences, settings: Settings, payload: Payload) { +async function createDmpSegment( + linkedinApiClient: LinkedInAudiences, + settings: Settings, + payload: Payload, + statsContext: StatsContext | undefined +) { + statsContext?.statsClient?.incr('oauth_app_api_call', 1, [...statsContext?.tags, `endpoint:create-dmpSegment`]) const res = await linkedinApiClient.createDmpSegment(settings, payload) const headers = res.headers.toJSON() return headers['x-linkedin-id'] From f45356fdeb42542d0d97a70330c84877e0db5564 Mon Sep 17 00:00:00 2001 From: Logan Luque <98849774+LLuque-twilio@users.noreply.github.com> Date: Thu, 30 Nov 2023 15:39:59 -0800 Subject: [PATCH 21/34] Remove import of lodash (#1749) --- packages/core/src/mapping-kit/value-keys.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/core/src/mapping-kit/value-keys.ts b/packages/core/src/mapping-kit/value-keys.ts index 90918798c7..e9c8caa4bd 100644 --- a/packages/core/src/mapping-kit/value-keys.ts +++ b/packages/core/src/mapping-kit/value-keys.ts @@ -1,9 +1,9 @@ -// import { isDirective } from './is-directive' -// eslint-disable-next-line lodash/import-scope -import _ from 'lodash' - type ValueType = 'enrichment' | 'function' | 'literal' | 'variable' +function isObject(value: any): value is object { + return value !== null && typeof value === 'object' +} + export interface DirectiveMetadata { _metadata?: { label?: string @@ -191,7 +191,7 @@ export function getFieldValueKeys(value: FieldValue): string[] { '@template': (input: TemplateDirective) => getTemplateKeys(input['@template']) })?.filter((k) => k) ?? [] ) - } else if (_.isObject(value)) { + } else if (isObject(value)) { return Object.values(value).flatMap(getFieldValueKeys) } return [] @@ -203,7 +203,7 @@ export function getFieldValueKeys(value: FieldValue): string[] { export function getRawKeys(input: FieldValue): string[] { if (isDirective(input)) { return getFieldValueKeys(input) - } else if (_.isObject(input)) { + } else if (isObject(input)) { return Object.values(input).flatMap(getFieldValueKeys) } return [] From 6ae452226bae1000cd100b52f293159842b30191 Mon Sep 17 00:00:00 2001 From: harsh-joshi99 <129737395+harsh-joshi99@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:46:39 +0530 Subject: [PATCH 22/34] Add performBatch in upsert profile (#1741) * Add performBatch in upsert profile * Add enable batching * Update error handling * Update error handling * Remove unused import --- .../src/destinations/klaviyo/functions.ts | 36 +++++- .../src/destinations/klaviyo/index.ts | 2 +- .../src/destinations/klaviyo/types.ts | 45 ++++++++ .../upsertProfile/__tests__/index.test.ts | 107 +++++++++++++++++- .../klaviyo/upsertProfile/generated-types.ts | 4 + .../klaviyo/upsertProfile/index.ts | 38 ++++++- 6 files changed, 225 insertions(+), 7 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index c7fe48c833..fe35b6b39a 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -1,6 +1,7 @@ import { APIError, RequestClient, DynamicFieldResponse } from '@segment/actions-core' import { API_URL, REVISION_DATE } from './config' -import { KlaviyoAPIError, ListIdResponse, ProfileData, listData } from './types' +import { ImportJobPayload, KlaviyoAPIError, ListIdResponse, ProfileData, listData } from './types' +import { Payload } from './upsertProfile/generated-types' export async function getListIdDynamicData(request: RequestClient): Promise { try { @@ -99,3 +100,36 @@ export function buildHeaders(authKey: string) { 'Content-Type': 'application/json' } } + +export const createImportJobPayload = (profiles: Payload[], listId?: string): { data: ImportJobPayload } => ({ + data: { + type: 'profile-bulk-import-job', + attributes: { + profiles: { + data: profiles.map(({ list_id, enable_batching, ...attributes }) => ({ + type: 'profile', + attributes + })) + } + }, + ...(listId + ? { + relationships: { + lists: { + data: [{ type: 'list', id: listId }] + } + } + } + : {}) + } +}) + +export const sendImportJobRequest = async (request: RequestClient, importJobPayload: { data: ImportJobPayload }) => { + return await request(`${API_URL}/profile-bulk-import-jobs/`, { + method: 'POST', + headers: { + revision: '2023-10-15.pre' + }, + json: importJobPayload + }) +} diff --git a/packages/destination-actions/src/destinations/klaviyo/index.ts b/packages/destination-actions/src/destinations/klaviyo/index.ts index e006457426..961a7bd669 100644 --- a/packages/destination-actions/src/destinations/klaviyo/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/index.ts @@ -18,7 +18,7 @@ const destination: AudienceDestinationDefinition = { scheme: 'custom', fields: { api_key: { - type: 'string', + type: 'password', label: 'API Key', description: `You can find this by going to Klaviyo's UI and clicking Account > Settings > API Keys > Create API Key`, required: true diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index db2fe1a272..9eca558977 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -80,3 +80,48 @@ export interface GetListResultContent { } }[] } + +export interface Location { + address1?: string | null + address2?: string | null + city?: string | null + region?: string | null + zip?: string | null + latitude?: string | null + longitude?: string | null + country?: string | null +} + +export interface ProfileAttributes { + email?: string + phone_number?: string + external_id?: string + first_name?: string + last_name?: string + organization?: string + title?: string + image?: string + location?: Location | null + properties?: Record + list_id?: string +} + +export interface ImportJobPayload { + type: string + attributes: { + profiles: { + data: { + type: string + attributes: ProfileAttributes + }[] + } + } + relationships?: { + lists: { + data: { + type: string + id: string + }[] + } + } +} diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts index ccfdd0897e..31bba326b6 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/__tests__/index.test.ts @@ -146,7 +146,7 @@ describe('Upsert Profile', () => { await expect( testDestination.testAction('upsertProfile', { event, settings, useDefaultMappings: true }) - ).rejects.toThrowError('An error occurred while processing the request') + ).rejects.toThrowError('Internal Server Error') }) it('should add a profile to a list if list_id is provided', async () => { @@ -263,3 +263,108 @@ describe('Upsert Profile', () => { expect(Functions.addProfileToList).toHaveBeenCalledWith(expect.anything(), profileId, listId) }) }) + +describe('Upsert Profile Batch', () => { + beforeEach(() => { + nock.cleanAll() + jest.resetAllMocks() + }) + + it('should discard profiles without email, phone_number, or external_id', async () => { + const events = [createTestEvent({ traits: { first_name: 'John', last_name: 'Doe' } })] + + const response = await testDestination.testBatchAction('upsertProfile', { + settings, + events, + useDefaultMappings: true + }) + + expect(response).toEqual([]) + }) + + it('should process profiles with and without list_ids separately', async () => { + const eventWithListId = createTestEvent({ + traits: { first_name: 'John', last_name: 'Doe', email: 'withlist@example.com', list_id: 'abc123' } + }) + const eventWithoutListId = createTestEvent({ + traits: { first_name: 'Jane', last_name: 'Smith', email: 'withoutlist@example.com' } + }) + + nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true, withList: true }) + nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true, withoutList: true }) + + const responseWithList = await testDestination.testBatchAction('upsertProfile', { + settings, + events: [eventWithListId], + mapping: { list_id: 'abc123' }, + useDefaultMappings: true + }) + + const responseWithoutList = await testDestination.testBatchAction('upsertProfile', { + settings, + events: [eventWithoutListId], + mapping: {}, + useDefaultMappings: true + }) + + expect(responseWithList[0]).toMatchObject({ + data: { success: true, withList: true } + }) + + expect(responseWithoutList[0]).toMatchObject({ + data: { success: true, withoutList: true } + }) + }) + + it('should process profiles with list_ids only', async () => { + const events = [createTestEvent({ traits: { email: 'withlist@example.com', list_id: 'abc123' } })] + + nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true, withList: true }) + + const response = await testDestination.testBatchAction('upsertProfile', { + settings, + events, + mapping: { list_id: 'abc123' }, + useDefaultMappings: true + }) + + expect(response[0].data).toMatchObject({ + success: true, + withList: true + }) + expect(response).toHaveLength(1) + }) + + it('should process profiles without list_ids only', async () => { + const events = [createTestEvent({ traits: { email: 'withoutlist@example.com' } })] + + nock(API_URL).post('/profile-bulk-import-jobs/').reply(200, { success: true, withoutList: true }) + + const response = await testDestination.testBatchAction('upsertProfile', { + settings, + events, + mapping: {}, + useDefaultMappings: true + }) + + expect(response[0].data).toMatchObject({ + success: true, + withoutList: true + }) + expect(response).toHaveLength(1) + }) + + it('should handle errors when sending profiles to Klaviyo', async () => { + const events = [createTestEvent({ traits: { email: 'error@example.com' } })] + + nock(API_URL).post('/profile-bulk-import-jobs/').reply(500, { error: 'Server error' }) + + await expect( + testDestination.testBatchAction('upsertProfile', { + settings, + events, + useDefaultMappings: true + }) + ).rejects.toThrow() + }) +}) diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts index 8c359e5c39..5941e33584 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/generated-types.ts @@ -5,6 +5,10 @@ export interface Payload { * Individual's email address. One of External ID, Phone Number and Email required. */ email?: string + /** + * When enabled, the action will use the klaviyo batch API. + */ + enable_batching?: boolean /** * Individual's phone number in E.164 format. If SMS is not enabled and if you use Phone Number as identifier, then you have to provide one of Email or External ID. */ diff --git a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts index 8aff65c904..24b30726cb 100644 --- a/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/upsertProfile/index.ts @@ -3,9 +3,9 @@ import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { API_URL } from '../config' -import { APIError, PayloadValidationError } from '@segment/actions-core' +import { PayloadValidationError } from '@segment/actions-core' import { KlaviyoAPIError, ProfileData } from '../types' -import { addProfileToList, getListIdDynamicData } from '../functions' +import { addProfileToList, createImportJobPayload, getListIdDynamicData, sendImportJobRequest } from '../functions' const action: ActionDefinition = { title: 'Upsert Profile', @@ -19,6 +19,11 @@ const action: ActionDefinition = { format: 'email', default: { '@path': '$.traits.email' } }, + enable_batching: { + type: 'boolean', + label: 'Batch Data to Klaviyo', + description: 'When enabled, the action will use the klaviyo batch API.' + }, phone_number: { label: 'Phone Number', description: `Individual's phone number in E.164 format. If SMS is not enabled and if you use Phone Number as identifier, then you have to provide one of Email or External ID.`, @@ -135,7 +140,7 @@ const action: ActionDefinition = { } }, perform: async (request, { payload }) => { - const { email, external_id, phone_number, list_id, ...otherAttributes } = payload + const { email, external_id, phone_number, list_id, enable_batching, ...otherAttributes } = payload if (!email && !phone_number && !external_id) { throw new PayloadValidationError('One of External ID, Phone Number and Email is required.') @@ -186,7 +191,32 @@ const action: ActionDefinition = { } } - throw new APIError('An error occurred while processing the request', 400) + throw error + } + }, + + performBatch: async (request, { payload }) => { + payload = payload.filter((profile) => profile.email || profile.external_id || profile.phone_number) + const profilesWithList = payload.filter((profile) => profile.list_id) + const profilesWithoutList = payload.filter((profile) => !profile.list_id) + + let importResponseWithList + let importResponseWithoutList + + if (profilesWithList.length > 0) { + const listId = profilesWithList[0].list_id + const importJobPayload = createImportJobPayload(profilesWithList, listId) + importResponseWithList = await sendImportJobRequest(request, importJobPayload) + } + + if (profilesWithoutList.length > 0) { + const importJobPayload = createImportJobPayload(profilesWithoutList) + importResponseWithoutList = await sendImportJobRequest(request, importJobPayload) + } + + return { + withList: importResponseWithList, + withoutList: importResponseWithoutList } } } From 84f33ca9bfc7e4f7e65b990818e2dbce5977d425 Mon Sep 17 00:00:00 2001 From: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:36:02 +0100 Subject: [PATCH 23/34] adding bucket web Integration (#1755) * adding bucket web Integration * updating yarn.lock --- .../destinations/bucket/package.json | 25 +++ .../bucket/src/__tests__/index.test.ts | 116 +++++++++++ .../bucket/src/generated-types.ts | 8 + .../bucket/src/group/__tests__/index.test.ts | 186 ++++++++++++++++++ .../bucket/src/group/generated-types.ts | 18 ++ .../destinations/bucket/src/group/index.ts | 49 +++++ .../src/identifyUser/__tests__/index.test.ts | 78 ++++++++ .../src/identifyUser/generated-types.ts | 14 ++ .../bucket/src/identifyUser/index.ts | 36 ++++ .../destinations/bucket/src/index.ts | 85 ++++++++ .../destinations/bucket/src/test-utils.ts | 60 ++++++ .../src/trackEvent/__tests__/index.test.ts | 159 +++++++++++++++ .../bucket/src/trackEvent/generated-types.ts | 18 ++ .../bucket/src/trackEvent/index.ts | 49 +++++ .../destinations/bucket/src/types.ts | 3 + .../destinations/bucket/tsconfig.json | 9 + packages/destinations-manifest/package.json | 3 +- yarn.lock | 60 ++++++ 18 files changed, 975 insertions(+), 1 deletion(-) create mode 100644 packages/browser-destinations/destinations/bucket/package.json create mode 100644 packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/generated-types.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/group/generated-types.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/group/index.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/index.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/test-utils.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts create mode 100644 packages/browser-destinations/destinations/bucket/src/types.ts create mode 100644 packages/browser-destinations/destinations/bucket/tsconfig.json diff --git a/packages/browser-destinations/destinations/bucket/package.json b/packages/browser-destinations/destinations/bucket/package.json new file mode 100644 index 0000000000..34ded4da11 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/package.json @@ -0,0 +1,25 @@ +{ + "name": "@segment/analytics-browser-actions-bucket", + "version": "1.0.0", + "license": "MIT", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "main": "./dist/cjs", + "module": "./dist/esm", + "scripts": { + "build": "yarn build:esm && yarn build:cjs", + "build:cjs": "tsc --module commonjs --outDir ./dist/cjs", + "build:esm": "tsc --outDir ./dist/esm" + }, + "typings": "./dist/esm", + "dependencies": { + "@bucketco/tracking-sdk": "^2.0.0", + "@segment/actions-core": "^3.90.0", + "@segment/browser-destination-runtime": "^1.20.0" + }, + "peerDependencies": { + "@segment/analytics-next": ">=1.55.0" + } +} diff --git a/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts new file mode 100644 index 0000000000..723ab6037b --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/__tests__/index.test.ts @@ -0,0 +1,116 @@ +import { Analytics, Context, User } from '@segment/analytics-next' +import bucketWebDestination, { destination } from '../index' +import { Subscription } from '@segment/browser-destination-runtime/types' +import { JSONArray } from '@segment/actions-core/*' +import { bucketTestHooks, getBucketCallLog } from '../test-utils' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'trackEvent', + name: 'Track Event', + enabled: true, + subscribe: 'type = "track"', + mapping: { + name: { + '@path': '$.name' + } + } + } +] + +describe('Bucket', () => { + bucketTestHooks() + + it('loads the Bucket SDK', async () => { + const [instance] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + jest.spyOn(destination, 'initialize') + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + + await instance.load(Context.system(), analyticsInstance) + expect(destination.initialize).toHaveBeenCalled() + + const scripts = Array.from(window.document.querySelectorAll('script')) + expect(scripts).toMatchInlineSnapshot(` + Array [ + , + ] + `) + + expect(window.bucket).toMatchObject({ + init: expect.any(Function), + user: expect.any(Function), + company: expect.any(Function), + track: expect.any(Function), + reset: expect.any(Function) + }) + }) + + it('resets the Bucket SDK', async () => { + const [instance] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + + await instance.load(Context.system(), analyticsInstance) + + analyticsInstance.reset() + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { method: 'reset', args: [] } + ]) + }) + + describe('when not logged in', () => { + it('initializes Bucket SDK', async () => { + const [instance] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + + await instance.load(Context.system(), analyticsInstance) + + expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey'] }]) + }) + }) + + describe('when logged in', () => { + it('initializes Bucket SDK and registers user', async () => { + const [instance] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + jest.spyOn(analyticsInstance, 'user').mockImplementation( + () => + ({ + id: () => 'test-user-id-1' + } as User) + ) + + await instance.load(Context.system(), analyticsInstance) + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { method: 'user', args: ['test-user-id-1', {}, { active: false }] } + ]) + }) + }) +}) diff --git a/packages/browser-destinations/destinations/bucket/src/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/generated-types.ts new file mode 100644 index 0000000000..ed3d76cd3b --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Your Bucket App tracking key, found on the tracking page. + */ + trackingKey: string +} diff --git a/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts new file mode 100644 index 0000000000..e9e4787fd4 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/group/__tests__/index.test.ts @@ -0,0 +1,186 @@ +import { Analytics, Context, User } from '@segment/analytics-next' +import bucketWebDestination, { destination } from '../../index' +import { Subscription } from '@segment/browser-destination-runtime/types' +import { JSONArray } from '@segment/actions-core/*' +import { bucketTestHooks, getBucketCallLog } from '../../test-utils' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'group', + name: 'Identify Company', + enabled: true, + subscribe: 'type = "group"', + mapping: { + groupId: { + '@path': '$.groupId' + }, + userId: { + '@path': '$.userId' + }, + traits: { + '@path': '$.traits' + } + } + } +] + +describe('Bucket.company', () => { + bucketTestHooks() + + describe('when logged in', () => { + describe('from analytics.js previous session', () => { + it('maps parameters correctly to Bucket', async () => { + const [bucketPlugin] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + jest.spyOn(analyticsInstance, 'user').mockImplementation( + () => + ({ + id: () => 'user-id-1' + } as User) + ) + await bucketPlugin.load(Context.system(), analyticsInstance) + + jest.spyOn(destination.actions.group, 'perform') + + await bucketPlugin.group?.( + new Context({ + type: 'group', + userId: 'user-id-1', + groupId: 'group-id-1', + traits: { + name: 'ACME INC' + } + }) + ) + + expect(destination.actions.group.perform).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + payload: { + userId: 'user-id-1', + groupId: 'group-id-1', + traits: { + name: 'ACME INC' + } + } + }) + ) + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { + method: 'user', + args: ['user-id-1', {}, { active: false }] + }, + { + method: 'company', + args: [ + 'group-id-1', + { + name: 'ACME INC' + }, + 'user-id-1' + ] + } + ]) + }) + }) + + describe('from am identify call', () => { + it('maps parameters correctly to Bucket', async () => { + const [bucketPlugin] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + await bucketPlugin.load(Context.system(), new Analytics({ writeKey: 'test-writekey' })) + + jest.spyOn(destination.actions.group, 'perform') + + // Bucket rejects group calls without previous identify calls + await window.bucket.user('user-id-1') + + await bucketPlugin.group?.( + new Context({ + type: 'group', + userId: 'user-id-1', + groupId: 'group-id-1', + traits: { + name: 'ACME INC' + } + }) + ) + + expect(destination.actions.group.perform).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + payload: { + userId: 'user-id-1', + groupId: 'group-id-1', + traits: { + name: 'ACME INC' + } + } + }) + ) + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { + method: 'user', + args: ['user-id-1'] + }, + { + method: 'company', + args: [ + 'group-id-1', + { + name: 'ACME INC' + }, + 'user-id-1' + ] + } + ]) + }) + }) + }) + + describe('when not logged in', () => { + it('should not call Bucket.group', async () => { + const [bucketPlugin] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + await bucketPlugin.load(Context.system(), analyticsInstance) + + jest.spyOn(destination.actions.group, 'perform') + + // Manually mimicking a group call without a userId. + // The analytics client will probably never do this if + // userId doesn't exist, since the subscription marks it as required + await bucketPlugin.group?.( + new Context({ + type: 'group', + anonymousId: 'anonymous-id-1', + groupId: 'group-id-1', + traits: { + name: 'ACME INC' + } + }) + ) + + // TODO: Ideally we should be able to assert that the destination action was never + // called, but couldn't figure out how to create an anlytics instance with the plugin + // and then trigger the full flow trhough analytics.group() with only an anonymous ID + // expect(destination.actions.group.perform).not.toHaveBeenCalled() + + expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey'] }]) + }) + }) +}) diff --git a/packages/browser-destinations/destinations/bucket/src/group/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/group/generated-types.ts new file mode 100644 index 0000000000..ff9f16892c --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/group/generated-types.ts @@ -0,0 +1,18 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Unique identifier for the company + */ + groupId: string + /** + * Unique identifier for the user + */ + userId: string + /** + * Additional information to associate with the Company in Bucket + */ + traits?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/destinations/bucket/src/group/index.ts b/packages/browser-destinations/destinations/bucket/src/group/index.ts new file mode 100644 index 0000000000..a4162da1e1 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/group/index.ts @@ -0,0 +1,49 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import type { Bucket } from '../types' + +const action: BrowserActionDefinition = { + title: 'Identify Company', + description: 'Creates or updates a Company in Bucket and associates the user with it', + platform: 'web', + defaultSubscription: 'type = "group"', + fields: { + groupId: { + type: 'string', + required: true, + description: 'Unique identifier for the company', + label: 'Company ID', + default: { + '@path': '$.groupId' + } + }, + userId: { + type: 'string', + required: true, + allowNull: false, + description: 'Unique identifier for the user', + label: 'User ID', + default: { + '@path': '$.userId' + } + }, + traits: { + type: 'object', + required: false, + description: 'Additional information to associate with the Company in Bucket', + label: 'Company Attributes', + default: { + '@path': '$.traits' + } + } + }, + perform: (bucket, { payload }) => { + // Ensure we never call Bucket.company() without a user ID + if (payload.userId) { + void bucket.company(payload.groupId, payload.traits, payload.userId) + } + } +} + +export default action diff --git a/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts new file mode 100644 index 0000000000..255a2c2050 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/__tests__/index.test.ts @@ -0,0 +1,78 @@ +import { Analytics, Context } from '@segment/analytics-next' +import bucketWebDestination, { destination } from '../../index' +import { Subscription } from '@segment/browser-destination-runtime/types' +import { JSONArray } from '@segment/actions-core/*' +import { bucketTestHooks, getBucketCallLog } from '../../test-utils' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'identifyUser', + name: 'Identify User', + enabled: true, + subscribe: 'type = "identify"', + mapping: { + userId: { + '@path': '$.userId' + }, + traits: { + '@path': '$.traits' + } + } + } +] + +describe('Bucket.user', () => { + bucketTestHooks() + + test('it maps event parameters correctly to bucket.user', async () => { + const [identifyEvent] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + await identifyEvent.load(Context.system(), new Analytics({ writeKey: 'test-writekey' })) + + jest.spyOn(destination.actions.identifyUser, 'perform') + + await identifyEvent.identify?.( + new Context({ + type: 'identify', + userId: 'user-id-1', + traits: { + name: 'John Doe', + email: 'test-email-2@gmail.com', + age: 42 + } + }) + ) + + expect(destination.actions.identifyUser.perform).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + payload: { + userId: 'user-id-1', + traits: { + name: 'John Doe', + email: 'test-email-2@gmail.com', + age: 42 + } + } + }) + ) + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { + method: 'user', + args: [ + 'user-id-1', + { + name: 'John Doe', + email: 'test-email-2@gmail.com', + age: 42 + } + ] + } + ]) + }) +}) diff --git a/packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts new file mode 100644 index 0000000000..2703e2cb3c --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/generated-types.ts @@ -0,0 +1,14 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Unique identifier for the User + */ + userId: string + /** + * Additional information to associate with the User in Bucket + */ + traits?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts b/packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts new file mode 100644 index 0000000000..4472b52cc9 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/identifyUser/index.ts @@ -0,0 +1,36 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import type { Bucket } from '../types' + +const action: BrowserActionDefinition = { + title: 'Identify User', + description: 'Creates or updates a user profile in Bucket. Also initializes Live Satisfaction', + platform: 'web', + defaultSubscription: 'type = "identify"', + fields: { + userId: { + type: 'string', + required: true, + description: 'Unique identifier for the User', + label: 'User ID', + default: { + '@path': '$.userId' + } + }, + traits: { + type: 'object', + required: false, + description: 'Additional information to associate with the User in Bucket', + label: 'User Attributes', + default: { + '@path': '$.traits' + } + } + }, + perform: (bucket, { payload }) => { + void bucket.user(payload.userId, payload.traits) + } +} + +export default action diff --git a/packages/browser-destinations/destinations/bucket/src/index.ts b/packages/browser-destinations/destinations/bucket/src/index.ts new file mode 100644 index 0000000000..c463095200 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/index.ts @@ -0,0 +1,85 @@ +import type { Settings } from './generated-types' +import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types' +import { browserDestination } from '@segment/browser-destination-runtime/shim' +import { Bucket } from './types' +import identifyUser from './identifyUser' +import trackEvent from './trackEvent' +import { defaultValues } from '@segment/actions-core' +import group from './group' + +declare global { + interface Window { + bucket: Bucket + } +} + +export const destination: BrowserDestinationDefinition = { + name: 'Bucket Web (Actions)', + description: + 'Loads the Bucket browser SDK, maps identify(), group() and track() events and enables LiveSatisfaction connections', + slug: 'bucket-web', + mode: 'device', + + presets: [ + { + name: 'Identify User', + subscribe: 'type = "identify"', + partnerAction: 'identifyUser', + mapping: defaultValues(identifyUser.fields), + type: 'automatic' + }, + { + name: 'Group', + subscribe: 'type = "group"', + partnerAction: 'group', + mapping: defaultValues(group.fields), + type: 'automatic' + }, + { + name: 'Track Event', + subscribe: 'type = "track"', + partnerAction: 'trackEvent', + mapping: defaultValues(trackEvent.fields), + type: 'automatic' + } + ], + + settings: { + trackingKey: { + description: 'Your Bucket App tracking key, found on the tracking page.', + label: 'Tracking Key', + type: 'string', + required: true + } + }, + + actions: { + identifyUser, + group, + trackEvent + }, + + initialize: async ({ settings, analytics }, deps) => { + await deps.loadScript('https://cdn.jsdelivr.net/npm/@bucketco/tracking-sdk@2') + await deps.resolveWhen(() => window.bucket != undefined, 100) + + window.bucket.init(settings.trackingKey) + + // If the analytics client already has a logged in user from a + // previous session or page, consider the user logged in. + // In this case we need to call `bucket.user()` to set the persisted + // user id in bucket and initialize Live Satisfaction + const segmentPersistedUserId = analytics.user().id() + if (segmentPersistedUserId) { + void window.bucket.user(segmentPersistedUserId, {}, { active: false }) + } + + analytics.on('reset', () => { + window.bucket.reset() + }) + + return window.bucket + } +} + +export default browserDestination(destination) diff --git a/packages/browser-destinations/destinations/bucket/src/test-utils.ts b/packages/browser-destinations/destinations/bucket/src/test-utils.ts new file mode 100644 index 0000000000..3c4cdb28a9 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/test-utils.ts @@ -0,0 +1,60 @@ +import nock from 'nock' +import { Bucket } from 'src/types' + +const bucketTestMock = ` +(() => { + const noop = () => {}; + + const bucketTestInterface = { + init: noop, + user: noop, + company: noop, + track: noop, + reset: noop + }; + + const callLog = []; + + window.bucket = new Proxy(bucketTestInterface, { + get(bucket, property) { + if (typeof bucket[property] === 'function') { + return (...args) => { + callLog.push({ method: property, args }) + return bucket[property](...args) + } + } + + if (property === 'callLog') { + return callLog; + } + } + }); +})(); +` + +export function bucketTestHooks() { + beforeAll(() => { + nock.disableNetConnect() + }) + + beforeEach(() => { + nock('https://cdn.jsdelivr.net').get('/npm/@bucketco/tracking-sdk@2').reply(200, bucketTestMock) + }) + + afterEach(function () { + if (!nock.isDone()) { + // @ts-expect-error no-unsafe-call + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + this.test.error(new Error('Not all nock interceptors were used!')) + nock.cleanAll() + } + }) + + afterAll(() => { + nock.enableNetConnect() + }) +} + +export function getBucketCallLog() { + return (window.bucket as unknown as { callLog: Array<{ method: keyof Bucket; args: Array }> }).callLog +} diff --git a/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts b/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..d7f583bc20 --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/__tests__/index.test.ts @@ -0,0 +1,159 @@ +import { Analytics, Context, User } from '@segment/analytics-next' +import bucketWebDestination, { destination } from '../../index' +import { Subscription } from '@segment/browser-destination-runtime/types' +import { JSONArray } from '@segment/actions-core/*' +import { bucketTestHooks, getBucketCallLog } from '../../test-utils' + +const subscriptions: Subscription[] = [ + { + partnerAction: 'trackEvent', + name: 'Track Event', + enabled: true, + subscribe: 'type = "track"', + mapping: { + name: { + '@path': '$.name' + }, + userId: { + '@path': '$.userId' + }, + properties: { + '@path': '$.properties' + } + } + } +] + +describe('trackEvent', () => { + bucketTestHooks() + + describe('when logged in', () => { + describe('from analytics.js previous session', () => { + it('maps parameters correctly to Bucket', async () => { + const [bucketPlugin] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + jest.spyOn(analyticsInstance, 'user').mockImplementation( + () => + ({ + id: () => 'user-id-1' + } as User) + ) + await bucketPlugin.load(Context.system(), analyticsInstance) + + jest.spyOn(destination.actions.trackEvent, 'perform') + + const properties = { property1: 'value1', property2: false } + await bucketPlugin.track?.( + new Context({ + type: 'track', + name: 'Button Clicked', + userId: 'user-id-1', + properties + }) + ) + + expect(destination.actions.trackEvent.perform).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + payload: { name: 'Button Clicked', userId: 'user-id-1', properties } + }) + ) + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { + method: 'user', + args: ['user-id-1', {}, { active: false }] + }, + { + method: 'track', + args: ['Button Clicked', properties, 'user-id-1'] + } + ]) + }) + }) + + describe('from am identify call', () => { + it('maps parameters correctly to Bucket', async () => { + const [bucketPlugin] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + await bucketPlugin.load(Context.system(), new Analytics({ writeKey: 'test-writekey' })) + + jest.spyOn(destination.actions.trackEvent, 'perform') + + // Bucket rejects group calls without previous identify calls + await window.bucket.user('user-id-1') + + const properties = { property1: 'value1', property2: false } + await bucketPlugin.track?.( + new Context({ + type: 'track', + name: 'Button Clicked', + userId: 'user-id-1', + properties + }) + ) + + expect(destination.actions.trackEvent.perform).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + payload: { name: 'Button Clicked', userId: 'user-id-1', properties } + }) + ) + + expect(getBucketCallLog()).toStrictEqual([ + { method: 'init', args: ['testTrackingKey'] }, + { + method: 'user', + args: ['user-id-1'] + }, + { + method: 'track', + args: ['Button Clicked', properties, 'user-id-1'] + } + ]) + }) + }) + }) + + describe('when not logged in', () => { + it('should not call Bucket.group', async () => { + const [bucketPlugin] = await bucketWebDestination({ + trackingKey: 'testTrackingKey', + subscriptions: subscriptions as unknown as JSONArray + }) + + const analyticsInstance = new Analytics({ writeKey: 'test-writekey' }) + await bucketPlugin.load(Context.system(), analyticsInstance) + + jest.spyOn(destination.actions.trackEvent, 'perform') + + // Manually mimicking a track call without a userId. + // The analytics client will probably never do this if + // userId doesn't exist, since the subscription marks it as required + const properties = { property1: 'value1', property2: false } + await bucketPlugin.track?.( + new Context({ + type: 'track', + name: 'Button Clicked', + anonymousId: 'user-id-1', + properties + }) + ) + + // TODO: Ideally we should be able to assert that the destination action was never + // called, but couldn't figure out how to create an anlytics instance with the plugin + // and then trigger the full flow trhough analytics.track() with only an anonymous ID + // expect(destination.actions.trackEvent.perform).not.toHaveBeenCalled() + + expect(getBucketCallLog()).toStrictEqual([{ method: 'init', args: ['testTrackingKey'] }]) + }) + }) +}) diff --git a/packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts b/packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts new file mode 100644 index 0000000000..2d7e5e4aed --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/generated-types.ts @@ -0,0 +1,18 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The event name + */ + name: string + /** + * Unique identifier for the user + */ + userId: string + /** + * Object containing the properties of the event + */ + properties?: { + [k: string]: unknown + } +} diff --git a/packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts b/packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts new file mode 100644 index 0000000000..29bf9a30fb --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/trackEvent/index.ts @@ -0,0 +1,49 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import type { Bucket } from '../types' + +const action: BrowserActionDefinition = { + title: 'Track Event', + description: 'Map a Segment track() event to Bucket', + platform: 'web', + defaultSubscription: 'type = "track"', + fields: { + name: { + description: 'The event name', + label: 'Event name', + required: true, + type: 'string', + default: { + '@path': '$.event' + } + }, + userId: { + type: 'string', + required: true, + allowNull: false, + description: 'Unique identifier for the user', + label: 'User ID', + default: { + '@path': '$.userId' + } + }, + properties: { + type: 'object', + required: false, + description: 'Object containing the properties of the event', + label: 'Event Properties', + default: { + '@path': '$.properties' + } + } + }, + perform: (bucket, { payload }) => { + // Ensure we never call Bucket.track() without a user ID + if (payload.userId) { + void bucket.track(payload.name, payload.properties, payload.userId) + } + } +} + +export default action diff --git a/packages/browser-destinations/destinations/bucket/src/types.ts b/packages/browser-destinations/destinations/bucket/src/types.ts new file mode 100644 index 0000000000..4307622bca --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/src/types.ts @@ -0,0 +1,3 @@ +import type bucket from '@bucketco/tracking-sdk' + +export type Bucket = typeof bucket diff --git a/packages/browser-destinations/destinations/bucket/tsconfig.json b/packages/browser-destinations/destinations/bucket/tsconfig.json new file mode 100644 index 0000000000..c2a7897afd --- /dev/null +++ b/packages/browser-destinations/destinations/bucket/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "baseUrl": "." + }, + "include": ["src"], + "exclude": ["dist", "**/__tests__"] +} diff --git a/packages/destinations-manifest/package.json b/packages/destinations-manifest/package.json index 953a5bf817..7c8463690e 100644 --- a/packages/destinations-manifest/package.json +++ b/packages/destinations-manifest/package.json @@ -47,6 +47,7 @@ "@segment/analytics-browser-actions-vwo": "^1.22.0", "@segment/analytics-browser-actions-wiseops": "^1.21.0", "@segment/analytics-browser-hubble-web": "^1.7.0", - "@segment/browser-destination-runtime": "^1.20.0" + "@segment/browser-destination-runtime": "^1.20.0", + "@segment/analytics-browser-actions-bucket": "^1.0.0" } } diff --git a/yarn.lock b/yarn.lock index 2a6a2154ad..15f845eb60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1021,6 +1021,17 @@ resolved "https://registry.yarnpkg.com/@braze/web-sdk/-/web-sdk-4.7.0.tgz#5adb930690d78dd3bc77a93dececde360a08d0f7" integrity sha512-fYdCyjlZqBswlebO8XmbPj04soLycHxnSvCQ/bWpi4OB00fz/ne34vv1LzIP3d0V5++jwjsutxdEi5mRiiMK1Q== +"@bucketco/tracking-sdk@^2.0.0": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@bucketco/tracking-sdk/-/tracking-sdk-2.1.6.tgz#f6373812c5a20af037b7696c0b69598810556790" + integrity sha512-LoB32PdaIPTyzmjjrgkoDkHIXXKhEFegrCshIxpgPHG3YrHapqjsjRuDgf9hQzfIpl9QAdRfpChTLWMtDkad7w== + dependencies: + "@floating-ui/dom" "^1.4.5" + cross-fetch "^4.0.0" + is-bundling-for-browser-or-node "^1.1.1" + js-cookie "^3.0.5" + preact "^10.16.0" + "@bufbuild/buf-darwin-arm64@1.28.0": version "1.28.0" resolved "https://registry.yarnpkg.com/@bufbuild/buf-darwin-arm64/-/buf-darwin-arm64-1.28.0.tgz#2a891aed84a6220628e802f9f3feb11023877e32" @@ -1110,6 +1121,26 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@floating-ui/core@^1.4.2": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.1.tgz#62707d7ec585d0929f882321a1b1f4ea9c680da5" + integrity sha512-QgcKYwzcc8vvZ4n/5uklchy8KVdjJwcOeI+HnnTNclJjs2nYsy23DOCf+sSV1kBwD9yDAoVKCkv/gEPzgQU3Pw== + dependencies: + "@floating-ui/utils" "^0.1.3" + +"@floating-ui/dom@^1.4.5": + version "1.5.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.5.3.tgz#54e50efcb432c06c23cd33de2b575102005436fa" + integrity sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA== + dependencies: + "@floating-ui/core" "^1.4.2" + "@floating-ui/utils" "^0.1.3" + +"@floating-ui/utils@^0.1.3": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" + integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== + "@fullstory/browser@^1.4.9": version "1.7.1" resolved "https://registry.yarnpkg.com/@fullstory/browser/-/browser-1.7.1.tgz#eb94fcb5e21b13a1b30de58951480ac344e61cdd" @@ -6746,6 +6777,13 @@ cross-fetch@^3.1.4: dependencies: node-fetch "2.6.1" +cross-fetch@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-4.0.0.tgz#f037aef1580bb3a1a35164ea2a848ba81b445983" + integrity sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g== + dependencies: + node-fetch "^2.6.12" + cross-spawn@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" @@ -9940,6 +9978,11 @@ is-buffer@^1.1.5, is-buffer@~1.1.6: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== +is-bundling-for-browser-or-node@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-bundling-for-browser-or-node/-/is-bundling-for-browser-or-node-1.1.1.tgz#dbbff6fc4ca4d0e8fbae26a404e135c80462fe7e" + integrity sha512-QjaU/+InR3DN5qVlaWgRJEuvz6CdP5jtGp37dxuvlY693AEuNNgmPGfDXXzkdRmJD+GqWSAhIQW1GTQXN60LPA== + is-callable@^1.1.3: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -10968,6 +11011,11 @@ js-cookie@3.0.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -12599,6 +12647,13 @@ node-fetch@2.6.7, node-fetch@^2.6.7: dependencies: whatwg-url "^5.0.0" +node-fetch@^2.6.12: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + node-forge@^1: version "1.3.1" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" @@ -13940,6 +13995,11 @@ postcss@^8.2.15, postcss@^8.3.5: picocolors "^0.2.1" source-map-js "^0.6.2" +preact@^10.16.0: + version "10.19.2" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.2.tgz#841797620dba649aaac1f8be42d37c3202dcea8b" + integrity sha512-UA9DX/OJwv6YwP9Vn7Ti/vF80XL+YA5H2l7BpCtUr3ya8LWHFzpiO5R+N7dN16ujpIxhekRFuOOF82bXX7K/lg== + prelude-ls@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" From 678181f48e5398b86676ddce428397b1f5ff68c3 Mon Sep 17 00:00:00 2001 From: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Date: Mon, 4 Dec 2023 13:48:13 +0100 Subject: [PATCH 24/34] registering bucket web integration --- packages/destinations-manifest/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/destinations-manifest/src/index.ts b/packages/destinations-manifest/src/index.ts index 476fc9b2e8..f3bc97c47d 100644 --- a/packages/destinations-manifest/src/index.ts +++ b/packages/destinations-manifest/src/index.ts @@ -64,3 +64,4 @@ register('652d4cf5e00c0147e6eaf5e7', '@segment/analytics-browser-actions-jimo') register('6261a8b6cb4caa70e19116e8', '@segment/analytics-browser-actions-snap-plugins') register('6554e468e280fb14fbb4433c', '@segment/analytics-browser-actions-replaybird') register('656773f0bd79a3676ab2733d', '@segment/analytics-browser-actions-1flow') +register('656dc9330d1863a8870bacd1', '@segment/analytics-browser-actions-bucket') From 4f3064d0020372275df1860c54178d4d8d76c654 Mon Sep 17 00:00:00 2001 From: Lukas Boehler Date: Mon, 4 Dec 2023 14:27:47 +0100 Subject: [PATCH 25/34] Gleap cloud action (#1745) * Added Gleap destination action * Added Gleap destination action * Updated Gleap integration based on PR review. * Updated Gleap integration based on PR review. --- .../__snapshots__/snapshot.test.ts.snap | 56 ++++++ .../gleap/__tests__/index.test.ts | 46 +++++ .../gleap/__tests__/snapshot.test.ts | 77 ++++++++ .../src/destinations/gleap/generated-types.ts | 8 + .../identifyContact/__tests__/index.test.ts | 23 +++ .../gleap/identifyContact/generated-types.ts | 62 +++++++ .../gleap/identifyContact/index.ts | 164 ++++++++++++++++++ .../src/destinations/gleap/index.ts | 57 ++++++ .../gleap/trackEvent/__tests__/index.test.ts | 21 +++ .../gleap/trackEvent/generated-types.ts | 30 ++++ .../destinations/gleap/trackEvent/index.ts | 119 +++++++++++++ 11 files changed, 663 insertions(+) create mode 100644 packages/destination-actions/src/destinations/gleap/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/destination-actions/src/destinations/gleap/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/gleap/__tests__/snapshot.test.ts create mode 100644 packages/destination-actions/src/destinations/gleap/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/gleap/identifyContact/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/gleap/identifyContact/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/gleap/identifyContact/index.ts create mode 100644 packages/destination-actions/src/destinations/gleap/index.ts create mode 100644 packages/destination-actions/src/destinations/gleap/trackEvent/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/gleap/trackEvent/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/gleap/trackEvent/index.ts diff --git a/packages/destination-actions/src/destinations/gleap/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/gleap/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..7886a1d8c0 --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,56 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Testing snapshot for actions-gleap destination: identifyContact action - all fields 1`] = ` +Object { + "companyId": "5(nuZw&pud4S", + "companyName": "5(nuZw&pud4S", + "createdAt": "2021-02-01T00:00:00.000Z", + "email": "perum@rakunar.dm", + "lang": "5(nuZw&pud4S", + "lastActivity": "2021-02-01T00:00:00.000Z", + "lastPageView": Object { + "date": "2021-02-01T00:00:00.000Z", + "page": "5(nuZw&pud4S", + }, + "name": "5(nuZw&pud4S 5(nuZw&pud4S", + "phone": "5(nuZw&pud4S", + "plan": "5(nuZw&pud4S", + "testType": "5(nuZw&pud4S", + "userId": "5(nuZw&pud4S", + "value": -3360299155456, +} +`; + +exports[`Testing snapshot for actions-gleap destination: identifyContact action - required fields 1`] = ` +Object { + "userId": "5(nuZw&pud4S", +} +`; + +exports[`Testing snapshot for actions-gleap destination: trackEvent action - all fields 1`] = ` +Object { + "events": Array [ + Object { + "data": Object { + "page": "5h*@HiWdC&Q2#9YhdZ&0", + }, + "date": "2021-02-01T00:00:00.000Z", + "name": "pageView", + "userId": "5h*@HiWdC&Q2#9YhdZ&0", + }, + ], +} +`; + +exports[`Testing snapshot for actions-gleap destination: trackEvent action - required fields 1`] = ` +Object { + "events": Array [ + Object { + "data": Object {}, + "date": "2021-02-01T00:00:00.000Z", + "name": "pageView", + "userId": "5h*@HiWdC&Q2#9YhdZ&0", + }, + ], +} +`; diff --git a/packages/destination-actions/src/destinations/gleap/__tests__/index.test.ts b/packages/destination-actions/src/destinations/gleap/__tests__/index.test.ts new file mode 100644 index 0000000000..bc32598daa --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/__tests__/index.test.ts @@ -0,0 +1,46 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import nock from 'nock' +import Definition from '../index' + +const testDestination = createTestIntegration(Definition) +const endpoint = 'https://api.gleap.io' + +describe('Gleap (actions)', () => { + describe('testAuthentication', () => { + it('should validate authentication inputs', async () => { + nock(endpoint).get('/admin/auth').reply(200, {}) + const authData = { + apiToken: '1234' + } + + await expect(testDestination.testAuthentication(authData)).resolves.not.toThrowError() + }) + + it('should fail on authentication failure', async () => { + nock(endpoint).get('/admin/auth').reply(404, {}) + const authData = { + apiToken: '1234' + } + + await expect(testDestination.testAuthentication(authData)).rejects.toThrowError( + new Error('Credentials are invalid: 404 Not Found') + ) + }) + }) + + describe('onDelete', () => { + it('should delete a user with a given userId', async () => { + const userId = '9999' + const event = createTestEvent({ userId: '9999' }) + nock(endpoint).delete(`/admin/contacts/${userId}`).reply(200, {}) + + if (testDestination.onDelete) { + const response = await testDestination.onDelete(event, { + apiToken: '1234' + }) + expect(response.status).toBe(200) + expect(response.data).toMatchObject({}) + } + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/gleap/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/gleap/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..a8234e54dc --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/__tests__/snapshot.test.ts @@ -0,0 +1,77 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { generateTestData } from '../../../lib/test-data' +import destination from '../index' +import nock from 'nock' + +const testDestination = createTestIntegration(destination) +const destinationSlug = 'actions-gleap' + +describe(`Testing snapshot for ${destinationSlug} destination:`, () => { + for (const actionSlug in destination.actions) { + it(`${actionSlug} action - required fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, true) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + + expect(request.headers).toMatchSnapshot() + }) + + it(`${actionSlug} action - all fields`, async () => { + const seedName = `${destinationSlug}#${actionSlug}` + const action = destination.actions[actionSlug] + const [eventData, settingsData] = generateTestData(seedName, destination, action, false) + + nock(/.*/).persist().get(/.*/).reply(200) + nock(/.*/).persist().post(/.*/).reply(200) + nock(/.*/).persist().put(/.*/).reply(200) + + const event = createTestEvent({ + properties: eventData + }) + + const responses = await testDestination.testAction(actionSlug, { + event: event, + mapping: event.properties, + settings: settingsData, + auth: undefined + }) + + const request = responses[0].request + const rawBody = await request.text() + + try { + const json = JSON.parse(rawBody) + expect(json).toMatchSnapshot() + return + } catch (err) { + expect(rawBody).toMatchSnapshot() + } + }) + } +}) diff --git a/packages/destination-actions/src/destinations/gleap/generated-types.ts b/packages/destination-actions/src/destinations/gleap/generated-types.ts new file mode 100644 index 0000000000..c04e1a03ac --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Found in `Project settings` -> `Secret API token`. + */ + apiToken: string +} diff --git a/packages/destination-actions/src/destinations/gleap/identifyContact/__tests__/index.test.ts b/packages/destination-actions/src/destinations/gleap/identifyContact/__tests__/index.test.ts new file mode 100644 index 0000000000..9e00403d93 --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/identifyContact/__tests__/index.test.ts @@ -0,0 +1,23 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import nock from 'nock' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) +const endpoint = 'https://api.gleap.io' + +describe('Gleap.identifyContact', () => { + it('should identify a user', async () => { + const event = createTestEvent({ + traits: { name: 'example user', email: 'user@example.com', userId: 'example-129394' } + }) + + nock(`${endpoint}`).post(`/admin/identify`).reply(200, {}) + + const responses = await testDestination.testAction('identifyContact', { + event, + useDefaultMappings: true + }) + + expect(responses[0].status).toBe(200) + }) +}) diff --git a/packages/destination-actions/src/destinations/gleap/identifyContact/generated-types.ts b/packages/destination-actions/src/destinations/gleap/identifyContact/generated-types.ts new file mode 100644 index 0000000000..c9bacf21ab --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/identifyContact/generated-types.ts @@ -0,0 +1,62 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique identifier for the contact. + */ + userId: string + /** + * The contact's first name. + */ + firstName?: string + /** + * The contact's last name. + */ + lastName?: string + /** + * The contact's email address. + */ + email?: string + /** + * The contact's phone number. + */ + phone?: string + /** + * The contact's company name. + */ + companyName?: string + /** + * The contact's compan ID + */ + companyId?: string + /** + * The user's language. + */ + lang?: string + /** + * The user's subscription plan. + */ + plan?: string + /** + * The user's value. + */ + value?: number + /** + * The page where the contact was last seen. + */ + lastPageView?: string + /** + * The time specified for when a contact signed up. + */ + createdAt?: string | number + /** + * The time when the contact was last seen. + */ + lastActivity?: string | number + /** + * The custom attributes which are set for the contact. + */ + customAttributes?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/gleap/identifyContact/index.ts b/packages/destination-actions/src/destinations/gleap/identifyContact/index.ts new file mode 100644 index 0000000000..bdecc4704d --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/identifyContact/index.ts @@ -0,0 +1,164 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import omit from 'lodash/omit' +import pick from 'lodash/pick' + +const action: ActionDefinition = { + title: 'Identify Contact', + description: 'Create or update a contact in Gleap', + defaultSubscription: 'type = "identify"', + fields: { + userId: { + type: 'string', + required: true, + description: 'A unique identifier for the contact.', + label: 'User ID', + default: { + '@path': '$.userId' + } + }, + firstName: { + type: 'string', + description: "The contact's first name.", + label: 'First name', + default: { + '@path': '$.properties.first_name' + } + }, + lastName: { + type: 'string', + description: "The contact's last name.", + label: 'Last name', + default: { + '@path': '$.properties.last_name' + } + }, + email: { + type: 'string', + description: "The contact's email address.", + label: 'Email Address', + format: 'email', + default: { '@path': '$.traits.email' } + }, + phone: { + label: 'Phone Number', + description: "The contact's phone number.", + type: 'string', + default: { + '@path': '$.traits.phone' + } + }, + companyName: { + label: 'Company Name', + description: "The contact's company name.", + type: 'string', + default: { + '@path': '$.traits.company.name' + } + }, + companyId: { + label: 'Company ID', + description: "The contact's compan ID", + type: 'string', + default: { + '@path': '$.traits.company.id' + } + }, + lang: { + label: 'Language', + description: "The user's language.", + type: 'string', + required: false, + default: { '@path': '$.context.locale' } + }, + plan: { + label: 'Subscription Plan', + description: "The user's subscription plan.", + type: 'string', + required: false, + default: { '@path': '$.traits.plan' } + }, + value: { + label: 'User Value', + description: "The user's value.", + type: 'number', + required: false + }, + lastPageView: { + label: 'Last Page View', + type: 'string', + description: 'The page where the contact was last seen.', + default: { + '@path': '$.context.page.url' + } + }, + createdAt: { + label: 'Signed Up Timestamp', + type: 'datetime', + description: 'The time specified for when a contact signed up.' + }, + lastActivity: { + label: 'Last Seen Timestamp', + type: 'datetime', + description: 'The time when the contact was last seen.', + default: { + '@path': '$.timestamp' + } + }, + customAttributes: { + label: 'Custom Attributes', + description: 'The custom attributes which are set for the contact.', + type: 'object', + defaultObjectUI: 'keyvalue', + default: { + '@path': '$.traits' + } + } + }, + perform: async (request, { payload }) => { + // Map the payload to the correct format. + const defaultUserFields = [ + 'userId', + 'email', + 'phone', + 'companyName', + 'companyId', + 'lang', + 'plan', + 'value', + 'createdAt', + 'lastActivity' + ] + + const identifyPayload: any = { + // Add the name if it exists. + ...(payload.firstName || payload.lastName + ? { + name: `${payload.firstName} ${payload.lastName}`.trim() + } + : {}), + + // Pick the default user fields. + ...pick(payload, defaultUserFields), + + // Add custom data but omit the default user fields. + ...omit(payload.customAttributes, [...defaultUserFields, 'firstName', 'lastName']) + } + + // Map the lastPageView and lastActivity to the correct format. + if (payload.lastPageView) { + identifyPayload.lastPageView = { + page: payload.lastPageView, + date: payload.lastActivity + } + } + + return request('https://api.gleap.io/admin/identify', { + method: 'POST', + json: identifyPayload + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/gleap/index.ts b/packages/destination-actions/src/destinations/gleap/index.ts new file mode 100644 index 0000000000..0abfcbb33b --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/index.ts @@ -0,0 +1,57 @@ +import { DestinationDefinition, IntegrationError } from '@segment/actions-core' +import type { Settings } from './generated-types' +import identifyContact from './identifyContact' +import trackEvent from './trackEvent' + +const destination: DestinationDefinition = { + name: 'Gleap (Action)', + slug: 'gleap-cloud-actions', + description: 'Send Segment analytics events and user profile data to Gleap', + mode: 'cloud', + + authentication: { + scheme: 'custom', + fields: { + apiToken: { + type: 'string', + label: 'Secret API token', + description: 'Found in `Project settings` -> `Secret API token`.', + required: true + } + }, + testAuthentication: async (request) => { + // The auth endpoint checks if the API token is valid + // https://api.gleap.io/admin/auth. + + return await request('https://api.gleap.io/admin/auth') + } + }, + extendRequest({ settings }) { + return { + headers: { + 'Api-Token': settings.apiToken + } + } + }, + + /** + * Delete a contact from Gleap when a user is deleted in Segment. Use the `userId` to find the contact in Gleap. + */ + onDelete: async (request, { payload }) => { + const userId = payload.userId as string + if (userId) { + return request(`https://api.gleap.io/admin/contacts/${userId}`, { + method: 'DELETE' + }) + } else { + throw new IntegrationError('No unique contact found', 'Contact not found', 404) + } + }, + + actions: { + identifyContact, + trackEvent + } +} + +export default destination diff --git a/packages/destination-actions/src/destinations/gleap/trackEvent/__tests__/index.test.ts b/packages/destination-actions/src/destinations/gleap/trackEvent/__tests__/index.test.ts new file mode 100644 index 0000000000..36e3f8051a --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/trackEvent/__tests__/index.test.ts @@ -0,0 +1,21 @@ +import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import nock from 'nock' +import Destination from '../../index' + +const testDestination = createTestIntegration(Destination) +const endpoint = 'https://api.gleap.io' + +describe('Gleap.trackEvent', () => { + it('should create an event with name and userId', async () => { + const event = createTestEvent({ event: 'Segment Test Event Name 3', userId: 'user1234' }) + + nock(`${endpoint}`).post(`/admin/track`).reply(200, {}) + + const responses = await testDestination.testAction('trackEvent', { + event, + useDefaultMappings: true + }) + + expect(responses[0].status).toBe(200) + }) +}) diff --git a/packages/destination-actions/src/destinations/gleap/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/gleap/trackEvent/generated-types.ts new file mode 100644 index 0000000000..4920980b99 --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/trackEvent/generated-types.ts @@ -0,0 +1,30 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The name of the event that occurred. Names are treated as case insensitive. Periods and dollar signs in event names are replaced with hyphens. + */ + eventName?: string + /** + * The type of the Segment event + */ + type: string + /** + * The associated page url of the Segment event + */ + pageUrl?: string + /** + * The time the event took place in ISO 8601 format. Segment will convert to Unix before sending to Gleap. + */ + date: string | number + /** + * Your identifier for the user who performed the event. User ID is required. + */ + userId: string + /** + * Optional metadata describing the event. Each event can contain up to ten metadata key-value pairs. If you send more than ten keys, Gleap will ignore the rest. + */ + data?: { + [k: string]: unknown + } +} diff --git a/packages/destination-actions/src/destinations/gleap/trackEvent/index.ts b/packages/destination-actions/src/destinations/gleap/trackEvent/index.ts new file mode 100644 index 0000000000..9ba53f5bc4 --- /dev/null +++ b/packages/destination-actions/src/destinations/gleap/trackEvent/index.ts @@ -0,0 +1,119 @@ +import { ActionDefinition } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' + +const preparePayload = (payload: Payload) => { + const event = { + name: payload.eventName, + date: payload.date, + data: payload.data, + userId: payload.userId + } + + if (payload.type === 'page') { + event.name = 'pageView' + event.data = { + page: payload.pageUrl ?? payload.eventName + } + } else if (payload.type === 'screen') { + event.name = 'pageView' + event.data = { + page: payload.eventName + } + } + + return event +} + +const sendEvents = async (request: any, events: any[]) => { + return request('https://api.gleap.io/admin/track', { + method: 'POST', + json: { + events: events + } + }) +} + +const action: ActionDefinition = { + title: 'Track Event', + description: 'Submit an event to Gleap.', + defaultSubscription: 'type = "track" or type = "page" or type = "screen"', + fields: { + eventName: { + type: 'string', + required: false, + description: + 'The name of the event that occurred. Names are treated as case insensitive. Periods and dollar signs in event names are replaced with hyphens.', + label: 'Event Name', + default: { + '@if': { + exists: { '@path': '$.event' }, + then: { '@path': '$.event' }, + else: { '@path': '$.name' } + } + } + }, + type: { + type: 'string', + unsafe_hidden: true, + required: true, + description: 'The type of the Segment event', + label: 'Event Type', + choices: [ + { label: 'track', value: 'track' }, + { label: 'page', value: 'page' }, + { label: 'screen', value: 'screen' } + ], + default: { + '@path': '$.type' + } + }, + pageUrl: { + label: 'Event Page URL', + description: 'The associated page url of the Segment event', + type: 'string', + format: 'uri', + required: false, + unsafe_hidden: true, + default: { '@path': '$.context.page.url' } + }, + date: { + type: 'datetime', + required: true, + description: + 'The time the event took place in ISO 8601 format. Segment will convert to Unix before sending to Gleap.', + label: 'Event Timestamp', + default: { + '@path': '$.timestamp' + } + }, + userId: { + type: 'string', + required: true, + description: 'Your identifier for the user who performed the event. User ID is required.', + label: 'User ID', + default: { + '@path': '$.userId' + } + }, + data: { + type: 'object', + description: + 'Optional metadata describing the event. Each event can contain up to ten metadata key-value pairs. If you send more than ten keys, Gleap will ignore the rest.', + label: 'Event Metadata', + default: { + '@path': '$.properties' + } + } + }, + perform: async (request, { payload }) => { + const event = preparePayload(payload) + return sendEvents(request, [event]) + }, + performBatch: async (request, { payload }) => { + const events = payload.map(preparePayload) + return sendEvents(request, events) + } +} + +export default action From 62a97c1bb46417ca4736b4bce8c0a903187f58e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rcio=20Martins?= <77632139+marcio-absmartly@users.noreply.github.com> Date: Mon, 4 Dec 2023 16:24:30 +0100 Subject: [PATCH 26/34] Adjust timestamps for consistency with other destinations (#1715) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adjust timestamps for consistency with other destinations * Use the raw data's timestamp field instead of a mapping * Fix typing errors --------- Co-authored-by: Márcio Martins --- .../absmartly/__tests__/exposure.test.ts | 39 +++++++------- .../absmartly/__tests__/goal.test.ts | 49 ++++-------------- .../absmartly/__tests__/timestamp.test.ts | 7 +++ .../src/destinations/absmartly/event.ts | 22 ++++---- .../src/destinations/absmartly/exposure.ts | 51 ++++++++++++++----- .../src/destinations/absmartly/goal.ts | 31 ++--------- .../src/destinations/absmartly/segment.ts | 11 ++++ .../src/destinations/absmartly/timestamp.ts | 3 +- .../trackExposure/__tests__/index.test.ts | 14 +++-- .../trackExposure/generated-types.ts | 4 -- .../absmartly/trackExposure/index.ts | 12 +++-- .../trackGoal/__tests__/index.test.ts | 8 ++- .../absmartly/trackGoal/generated-types.ts | 8 --- .../destinations/absmartly/trackGoal/index.ts | 12 +++-- 14 files changed, 134 insertions(+), 137 deletions(-) create mode 100644 packages/destination-actions/src/destinations/absmartly/segment.ts diff --git a/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts b/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts index 121fa89cec..a900a07b5d 100644 --- a/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts +++ b/packages/destination-actions/src/destinations/absmartly/__tests__/exposure.test.ts @@ -8,14 +8,14 @@ jest.mock('../event') describe('sendExposure()', () => { const settings = { collectorEndpoint: 'http://test.com', environment: 'dev', apiKey: 'testkey' } const payload: ExposurePayload = { - publishedAt: '2023-01-01T00:00:00.3Z', application: 'testapp', agent: 'test-sdk', exposure: { + publishedAt: 1672531200900, units: [{ type: 'anonymousId', value: 'testid' }], - exposures: [{ experiment: 'testexp', variant: 'testvar' }], + exposures: [{ experiment: 'testexp', variant: 'testvar', exposedAt: 1672531200300 }], goals: [], - attributes: [{ name: 'testattr', value: 'testval', setAt: 1238128318 }] + attributes: [{ name: 'testattr', value: 'testval', setAt: 1672531200200 }] } } @@ -25,6 +25,7 @@ describe('sendExposure()', () => { expect(() => sendExposure( request, + 1672531300000, { ...payload, exposure: { ...payload.exposure, units: null } @@ -35,6 +36,7 @@ describe('sendExposure()', () => { expect(() => sendExposure( request, + 1672531300000, { ...payload, exposure: { ...payload.exposure, units: [] } @@ -50,6 +52,7 @@ describe('sendExposure()', () => { expect(() => sendExposure( request, + 1672531300000, { ...payload, exposure: { ...payload.exposure, exposures: null } @@ -60,6 +63,7 @@ describe('sendExposure()', () => { expect(() => sendExposure( request, + 1672531300000, { ...payload, exposure: { ...payload.exposure, exposures: [] } @@ -75,6 +79,7 @@ describe('sendExposure()', () => { expect(() => sendExposure( request, + 1672531300000, { ...payload, exposure: { ...payload.exposure, goals: [{}] } @@ -84,31 +89,21 @@ describe('sendExposure()', () => { ).toThrowError(PayloadValidationError) }) - it('should throw on invalid publishedAt', async () => { + it('should pass-through the exposure payload with adjusted timestamps', async () => { const request = jest.fn() - expect(() => sendExposure(request, { ...payload, publishedAt: 0 }, settings)).toThrowError(PayloadValidationError) - expect(() => - sendExposure( - request, - { - ...payload, - publishedAt: 'invalid date' - }, - settings - ) - ).toThrowError(PayloadValidationError) - }) - - it('should pass-through the exposure payload with adjusted publishedAt', async () => { - const request = jest.fn() - - await sendExposure(request, payload, settings) + await sendExposure(request, 1672531300000, payload, settings) expect(sendEvent).toHaveBeenCalledWith( request, settings, - { ...payload.exposure, publishedAt: 1672531200300 }, + { + ...payload.exposure, + historic: true, + publishedAt: 1672531300000, + exposures: [{ ...payload.exposure.exposures[0], exposedAt: 1672531299400 }], + attributes: [{ ...payload.exposure.attributes[0], setAt: 1672531299300 }] + }, payload.agent, payload.application ) diff --git a/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts b/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts index 8eff72495d..e41618247c 100644 --- a/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts +++ b/packages/destination-actions/src/destinations/absmartly/__tests__/goal.test.ts @@ -12,8 +12,6 @@ describe('sendGoal()', () => { anonymousId: 'testid' }, name: 'testgoal', - publishedAt: '2023-01-01T00:00:00.3Z', - achievedAt: '2023-01-01T00:00:00.000000Z', application: 'testapp', agent: 'test-sdk', properties: { @@ -24,10 +22,13 @@ describe('sendGoal()', () => { it('should throw on missing name', async () => { const request = jest.fn() - expect(() => sendGoal(request, { ...payload, name: '' }, settings)).toThrowError(PayloadValidationError) + expect(() => sendGoal(request, 1672531300000, { ...payload, name: '' }, settings)).toThrowError( + PayloadValidationError + ) expect(() => sendGoal( request, + 1672531300000, { ...payload, name: null @@ -38,6 +39,7 @@ describe('sendGoal()', () => { expect(() => sendGoal( request, + 1672531300000, { ...payload, name: undefined @@ -47,44 +49,13 @@ describe('sendGoal()', () => { ).toThrowError(PayloadValidationError) }) - it('should throw on invalid publishedAt', async () => { - const request = jest.fn() - - expect(() => sendGoal(request, { ...payload, publishedAt: 0 }, settings)).toThrowError(PayloadValidationError) - expect(() => - sendGoal( - request, - { - ...payload, - publishedAt: 'invalid date' - }, - settings - ) - ).toThrowError(PayloadValidationError) - }) - - it('should throw on invalid achievedAt', async () => { - const request = jest.fn() - - expect(() => sendGoal(request, { ...payload, achievedAt: 0 }, settings)).toThrowError(PayloadValidationError) - expect(() => - sendGoal( - request, - { - ...payload, - achievedAt: 'invalid date' - }, - settings - ) - ).toThrowError(PayloadValidationError) - }) - it('should throw on invalid properties', async () => { const request = jest.fn() expect(() => sendGoal( request, + 1672531300000, { ...payload, properties: 'bleh' @@ -95,6 +66,7 @@ describe('sendGoal()', () => { expect(() => sendGoal( request, + 1672531300000, { ...payload, properties: 0 @@ -107,18 +79,19 @@ describe('sendGoal()', () => { it('should send event with correct format', async () => { const request = jest.fn() - await sendGoal(request, payload, settings) + await sendGoal(request, 1672531300000, payload, settings) expect(sendEvent).toHaveBeenCalledWith( request, settings, { - publishedAt: 1672531200300, + historic: true, + publishedAt: 1672531300000, units: mapUnits(payload), goals: [ { name: payload.name, - achievedAt: 1672531200000, + achievedAt: 1672531300000, properties: payload.properties ?? null } ] diff --git a/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts b/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts index 8fac1e2770..d040998298 100644 --- a/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts +++ b/packages/destination-actions/src/destinations/absmartly/__tests__/timestamp.test.ts @@ -36,4 +36,11 @@ describe('unixTimestampOf()', () => { expect(unixTimestampOf('2023-01-01T00:00:00.00345Z')).toBe(1672531200003) expect(unixTimestampOf('2023-01-01T00:00:00.003456Z')).toBe(1672531200003) }) + + it('should convert Date to number representing Unix timestamp in milliseconds', async () => { + expect(unixTimestampOf(new Date('2000-01-01T00:00:00Z'))).toBe(946684800000) + expect(unixTimestampOf(new Date('2023-01-01T00:00:00.003Z'))).toBe(1672531200003) + expect(unixTimestampOf(new Date('2023-01-01T00:00:00.00345Z'))).toBe(1672531200003) + expect(unixTimestampOf(new Date('2023-01-01T00:00:00.003456Z'))).toBe(1672531200003) + }) }) diff --git a/packages/destination-actions/src/destinations/absmartly/event.ts b/packages/destination-actions/src/destinations/absmartly/event.ts index a45074afc4..cf64f4544d 100644 --- a/packages/destination-actions/src/destinations/absmartly/event.ts +++ b/packages/destination-actions/src/destinations/absmartly/event.ts @@ -1,4 +1,4 @@ -import { InputField, JSONObject, ModifiedResponse, RequestClient } from '@segment/actions-core' +import { InputField, ModifiedResponse, RequestClient } from '@segment/actions-core' import { Settings } from './generated-types' import { PublishRequestUnit } from './unit' import { PublishRequestAttribute } from './attribute' @@ -6,30 +6,26 @@ import { PublishRequestGoal } from './goal' import { Data } from 'ws' export interface PublishRequestEvent { + historic?: boolean publishedAt: number units: PublishRequestUnit[] goals?: PublishRequestGoal[] - exposures?: JSONObject[] + exposures?: { + name: string + variant: number + exposedAt: number + assigned: boolean + eligible: boolean + }[] attributes?: PublishRequestAttribute[] } export interface DefaultPayload { - publishedAt: string | number agent?: string application?: string } export const defaultEventFields: Record = { - publishedAt: { - label: 'Event Sent Time', - type: 'datetime', - required: true, - description: - 'Exact timestamp when the event was sent (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number', - default: { - '@path': '$.sentAt' - } - }, agent: { label: 'Agent', type: 'string', diff --git a/packages/destination-actions/src/destinations/absmartly/exposure.ts b/packages/destination-actions/src/destinations/absmartly/exposure.ts index ddfe383a07..110f4f2cda 100644 --- a/packages/destination-actions/src/destinations/absmartly/exposure.ts +++ b/packages/destination-actions/src/destinations/absmartly/exposure.ts @@ -1,4 +1,10 @@ -import { InputField, ModifiedResponse, PayloadValidationError, RequestClient } from '@segment/actions-core' +import { + InputField, + JSONPrimitive, + ModifiedResponse, + PayloadValidationError, + RequestClient +} from '@segment/actions-core' import { defaultEventFields, DefaultPayload, PublishRequestEvent, sendEvent } from './event' import { Settings } from './generated-types' import { isValidTimestamp, unixTimestampOf } from './timestamp' @@ -23,11 +29,18 @@ export const defaultExposureFields: Record = { ...defaultEventFields } -function isValidExposure(exposure?: PublishRequestEvent | Record): exposure is PublishRequestEvent { +function isValidExposureRequest( + exposure?: PublishRequestEvent | Record +): exposure is PublishRequestEvent { if (exposure == null || typeof exposure != 'object') { return false } + const publishedAt = exposure['publishedAt'] as JSONPrimitive + if (!isValidTimestamp(publishedAt)) { + return false + } + const units = exposure['units'] if (!Array.isArray(units) || units.length == 0) { return false @@ -38,6 +51,10 @@ function isValidExposure(exposure?: PublishRequestEvent | Record typeof x['exposedAt'] !== 'number' || !isValidTimestamp(x['exposedAt']))) { + return false + } + const goals = exposure['goals'] if (goals != null && (!Array.isArray(goals) || goals.length > 0)) { return false @@ -53,32 +70,40 @@ function isValidExposure(exposure?: PublishRequestEvent | Record> { - if (!isValidTimestamp(payload.publishedAt)) { - throw new PayloadValidationError( - 'Exposure `publishedAt` is required to be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number' - ) - } - - const exposure = payload.exposure - if (exposure == null || typeof exposure != 'object') { + const exposureRequest = payload.exposure as unknown as PublishRequestEvent + if (exposureRequest == null || typeof exposureRequest != 'object') { throw new PayloadValidationError('Field `exposure` is required to be an object when tracking exposures') } - if (!isValidExposure(exposure)) { + if (!isValidExposureRequest(exposureRequest)) { throw new PayloadValidationError( 'Field `exposure` is malformed or contains goals. Ensure you are sending a valid ABsmartly exposure payload without goals.' ) } + const offset = timestamp - unixTimestampOf(exposureRequest.publishedAt) + const exposures = exposureRequest.exposures?.map((x) => ({ + ...x, + exposedAt: x.exposedAt + offset + })) + const attributes = exposureRequest.attributes?.map((x) => ({ + ...x, + setAt: x.setAt + offset + })) + return sendEvent( request, settings, { - ...exposure, - publishedAt: unixTimestampOf(payload.publishedAt) + ...exposureRequest, + historic: true, + publishedAt: timestamp, + exposures, + attributes }, payload.agent, payload.application diff --git a/packages/destination-actions/src/destinations/absmartly/goal.ts b/packages/destination-actions/src/destinations/absmartly/goal.ts index 1481e4d994..7fc22e29ad 100644 --- a/packages/destination-actions/src/destinations/absmartly/goal.ts +++ b/packages/destination-actions/src/destinations/absmartly/goal.ts @@ -2,7 +2,7 @@ import { mapUnits, Units } from './unit' import { InputField, ModifiedResponse, PayloadValidationError, RequestClient } from '@segment/actions-core' import { sendEvent, PublishRequestEvent, defaultEventFields, DefaultPayload } from './event' import { Settings } from './generated-types' -import { isValidTimestamp, unixTimestampOf } from './timestamp' +import { unixTimestampOf } from './timestamp' import { Data } from 'ws' export interface PublishRequestGoal { @@ -13,7 +13,6 @@ export interface PublishRequestGoal { export interface GoalPayload extends Units, DefaultPayload { name: string - achievedAt: string | number properties?: null | Record } @@ -43,16 +42,6 @@ export const defaultGoalFields: Record = { '@path': '$.event' } }, - achievedAt: { - label: 'Goal Achievement Time', - type: 'datetime', - required: true, - description: - 'Exact timestamp when the goal was achieved (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number', - default: { - '@path': '$.originalTimestamp' - } - }, properties: { label: 'Goal Properties', type: 'object', @@ -67,6 +56,7 @@ export const defaultGoalFields: Record = { export function sendGoal( request: RequestClient, + timestamp: number, payload: GoalPayload, settings: Settings ): Promise> { @@ -74,29 +64,18 @@ export function sendGoal( throw new PayloadValidationError('Goal `name` is required to be a non-empty string') } - if (!isValidTimestamp(payload.publishedAt)) { - throw new PayloadValidationError( - 'Goal `publishedAt` is required to be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number' - ) - } - - if (!isValidTimestamp(payload.achievedAt)) { - throw new PayloadValidationError( - 'Goal `achievedAt` is required to be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number' - ) - } - if (payload.properties != null && typeof payload.properties != 'object') { throw new PayloadValidationError('Goal `properties` if present is required to be an object') } const event: PublishRequestEvent = { - publishedAt: unixTimestampOf(payload.publishedAt), + historic: true, + publishedAt: unixTimestampOf(timestamp), units: mapUnits(payload), goals: [ { name: payload.name, - achievedAt: unixTimestampOf(payload.achievedAt), + achievedAt: unixTimestampOf(timestamp), properties: payload.properties ?? null } ] diff --git a/packages/destination-actions/src/destinations/absmartly/segment.ts b/packages/destination-actions/src/destinations/absmartly/segment.ts new file mode 100644 index 0000000000..00fdffbfeb --- /dev/null +++ b/packages/destination-actions/src/destinations/absmartly/segment.ts @@ -0,0 +1,11 @@ +export interface RequestData { + rawData: { + timestamp: string + type: string + receivedAt: string + sentAt: string + } + rawMapping: Record + settings: Settings + payload: Payload +} diff --git a/packages/destination-actions/src/destinations/absmartly/timestamp.ts b/packages/destination-actions/src/destinations/absmartly/timestamp.ts index d9a195fb98..204a7d5f54 100644 --- a/packages/destination-actions/src/destinations/absmartly/timestamp.ts +++ b/packages/destination-actions/src/destinations/absmartly/timestamp.ts @@ -9,7 +9,8 @@ export function isValidTimestamp(timestamp: JSONPrimitive): boolean { return false } -export function unixTimestampOf(timestamp: JSONPrimitive): number { +export function unixTimestampOf(timestamp: JSONPrimitive | Date): number { if (typeof timestamp === 'number') return timestamp + if (timestamp instanceof Date) return timestamp.getTime() return Date.parse(timestamp as string) } diff --git a/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts b/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts index b826793df4..1d1c8850d6 100644 --- a/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/absmartly/trackExposure/__tests__/index.test.ts @@ -1,6 +1,8 @@ import nock from 'nock' import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core' import Destination from '../../index' +import { unixTimestampOf } from '../../timestamp' +import { PublishRequestEvent } from '../../event' const testDestination = createTestIntegration(Destination) @@ -17,7 +19,7 @@ describe('ABsmartly.trackExposure', () => { anonymousId: 'anon-123', properties: { exposure: { - publishedAt: 123, + publishedAt: 1602531300000, units: [{ type: 'anonymousId', uid: 'anon-123' }], exposures: [ { @@ -50,15 +52,19 @@ describe('ABsmartly.trackExposure', () => { useDefaultMappings: true }) + const timestamp = unixTimestampOf(exposureEvent.timestamp!) + const exposureRequest = exposureEvent.properties.exposure as PublishRequestEvent + expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) expect(await responses[0].request.json()).toStrictEqual({ - publishedAt: 1672531200100, + historic: true, + publishedAt: timestamp, units: [{ type: 'anonymousId', uid: 'anon-123' }], exposures: [ { assigned: true, - exposedAt: 1602531200000, + exposedAt: timestamp - (exposureRequest.publishedAt - exposureRequest.exposures?.[0].exposedAt), id: 10, name: 'test_experiment' } @@ -67,7 +73,7 @@ describe('ABsmartly.trackExposure', () => { { name: 'test', value: 'test', - setAt: 1602530000000 + setAt: timestamp - (exposureRequest.publishedAt - exposureRequest.attributes?.[0].setAt) } ] }) diff --git a/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts b/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts index cfade7555e..d95f8bd3cf 100644 --- a/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts +++ b/packages/destination-actions/src/destinations/absmartly/trackExposure/generated-types.ts @@ -7,10 +7,6 @@ export interface Payload { exposure: { [k: string]: unknown } - /** - * Exact timestamp when the event was sent (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number - */ - publishedAt: string | number /** * Optional agent identifier that originated the event. Used to identify which SDK generated the event. */ diff --git a/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts b/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts index 0c09c0d7ba..a0f1ea827c 100644 --- a/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts +++ b/packages/destination-actions/src/destinations/absmartly/trackExposure/index.ts @@ -1,7 +1,9 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { defaultExposureFields, sendExposure } from '../exposure' +import { defaultExposureFields, ExposurePayload, sendExposure } from '../exposure' +import { RequestData } from '../segment' +import { unixTimestampOf } from '../timestamp' const fields = { ...defaultExposureFields } @@ -10,8 +12,12 @@ const action: ActionDefinition = { description: 'Send an experiment exposure event to ABsmartly', fields: fields, defaultSubscription: 'type = "track" and event = "Experiment Viewed"', - perform: (request, { payload, settings }) => { - return sendExposure(request, payload, settings) + perform: (request, data) => { + const requestData = data as RequestData + const timestamp = unixTimestampOf(requestData.rawData.timestamp) + const payload = requestData.payload + const settings = requestData.settings + return sendExposure(request, timestamp, payload, settings) } } diff --git a/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts b/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts index c17abf3591..a13fc75625 100644 --- a/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/absmartly/trackGoal/__tests__/index.test.ts @@ -1,6 +1,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration, SegmentEvent } from '@segment/actions-core' import Destination from '../../index' +import { unixTimestampOf } from '../../timestamp' const testDestination = createTestIntegration(Destination) @@ -32,17 +33,20 @@ describe('ABsmartly.trackGoal', () => { useDefaultMappings: true }) + const timestamp = unixTimestampOf(baseEvent.timestamp!) + expect(responses.length).toBe(1) expect(responses[0].status).toBe(200) expect(await responses[0].request.json()).toStrictEqual({ - publishedAt: 1672531200100, + historic: true, + publishedAt: timestamp, units: [ { type: 'anonymousId', uid: 'anon-123' }, { type: 'userId', uid: '123' } ], goals: [ { - achievedAt: 1672531200000, + achievedAt: timestamp, name: 'Order Completed', properties: baseEvent.properties } diff --git a/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts b/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts index 13983dd560..26b0465b49 100644 --- a/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts +++ b/packages/destination-actions/src/destinations/absmartly/trackGoal/generated-types.ts @@ -11,20 +11,12 @@ export interface Payload { * The name of the goal to track */ name: string - /** - * Exact timestamp when the goal was achieved (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number - */ - achievedAt: string | number /** * Custom properties of the goal */ properties: { [k: string]: unknown } - /** - * Exact timestamp when the event was sent (measured by the client clock). Must be an ISO 8601 date-time string, or a Unix timestamp (milliseconds) number - */ - publishedAt: string | number /** * Optional agent identifier that originated the event. Used to identify which SDK generated the event. */ diff --git a/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts b/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts index 8ade0cec0a..324e747c53 100644 --- a/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts +++ b/packages/destination-actions/src/destinations/absmartly/trackGoal/index.ts @@ -1,7 +1,9 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { defaultGoalFields, sendGoal } from '../goal' +import { defaultGoalFields, GoalPayload, sendGoal } from '../goal' +import { RequestData } from '../segment' +import { unixTimestampOf } from '../timestamp' const fields = { ...defaultGoalFields } @@ -10,8 +12,12 @@ const action: ActionDefinition = { description: 'Send a goal event to ABsmartly', fields: fields, defaultSubscription: 'type = "track" and event != "Experiment Viewed"', - perform: (request, { payload, settings }) => { - return sendGoal(request, payload, settings) + perform: (request, data) => { + const requestData = data as RequestData + const timestamp = unixTimestampOf(requestData.rawData.timestamp) + const payload = requestData.payload + const settings = requestData.settings + return sendGoal(request, timestamp, payload, settings) } } From 277e37c92e4ea967b2649836095b31fd6122d172 Mon Sep 17 00:00:00 2001 From: Neeharika Kondipati <94875208+VenkataNeeharikaKondipati@users.noreply.github.com> Date: Mon, 4 Dec 2023 10:33:28 -0800 Subject: [PATCH 27/34] Turn click tracking off for unsubscribe links (#1753) --- .../engage/sendgrid/__tests__/send-email.test.ts | 8 ++++---- .../engage/sendgrid/sendEmail/SendEmailPerformer.ts | 9 ++++++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/destination-actions/src/destinations/engage/sendgrid/__tests__/send-email.test.ts b/packages/destination-actions/src/destinations/engage/sendgrid/__tests__/send-email.test.ts index 46d713025a..4ceda2c01d 100644 --- a/packages/destination-actions/src/destinations/engage/sendgrid/__tests__/send-email.test.ts +++ b/packages/destination-actions/src/destinations/engage/sendgrid/__tests__/send-email.test.ts @@ -764,7 +764,7 @@ describe.each([ const bodyHtml = '

Hi First Name, welcome to Segment

Unsubscribe | Manage Preferences' const replacedHtmlWithLink = - '

Hi First Name, welcome to Segment

Unsubscribe | Manage Preferences' + '

Hi First Name, welcome to Segment

Unsubscribe | Manage Preferences' const expectedSendGridRequest = { personalizations: [ { @@ -847,7 +847,7 @@ describe.each([ const bodyHtml = '

Hi First Name, welcome to Segment

Manage Preferences | Unsubscribe' const replacedHtmlWithLink = - '

Hi First Name, welcome to Segment

Unsubscribe' + '

Hi First Name, welcome to Segment

Unsubscribe' const expectedSendGridRequest = { personalizations: [ { @@ -930,7 +930,7 @@ describe.each([ const bodyHtml = '

Hi First Name, welcome to Segment. Here is an Unsubscribe link.

Unsubscribe | Manage Preferences' const replacedHtmlWithLink = - '

Hi First Name, welcome to Segment. Here is an Unsubscribe link.

Unsubscribe' + '

Hi First Name, welcome to Segment. Here is an Unsubscribe link.

Unsubscribe' const expectedSendGridRequest = { personalizations: [ { @@ -1844,7 +1844,7 @@ describe.each([ const bodyHtml = '

Hi First Name, welcome to Segment

Unsubscribe | Manage Preferences' const replacedHtmlWithLink = - '

Hi First Name, welcome to Segment

Unsubscribe | Manage Preferences' + '

Hi First Name, welcome to Segment

Unsubscribe | Manage Preferences' const expectedSendGridRequest = { personalizations: [ diff --git a/packages/destination-actions/src/destinations/engage/sendgrid/sendEmail/SendEmailPerformer.ts b/packages/destination-actions/src/destinations/engage/sendgrid/sendEmail/SendEmailPerformer.ts index 20ff390add..3a21745b15 100644 --- a/packages/destination-actions/src/destinations/engage/sendgrid/sendEmail/SendEmailPerformer.ts +++ b/packages/destination-actions/src/destinations/engage/sendgrid/sendEmail/SendEmailPerformer.ts @@ -348,7 +348,8 @@ export class SendEmailPerformer extends MessageSendPerformer _this.statsClient.incr('group_unsubscribe_link_missing', 1) $(this).attr('href', sendgridUnsubscribeLinkTag) } else { - $(this).attr('href', groupUnsubscribeLink) + $(this).removeAttr('href') + $(this).attr('clicktracking', 'off').attr('href', groupUnsubscribeLink) _this.logger?.info(`Group Unsubscribe link replaced`) _this.statsClient?.incr('replaced_group_unsubscribe_link', 1) } @@ -360,7 +361,8 @@ export class SendEmailPerformer extends MessageSendPerformer _this.statsClient?.incr('global_unsubscribe_link_missing', 1) $(this).attr('href', sendgridUnsubscribeLinkTag) } else { - $(this).attr('href', globalUnsubscribeLink) + $(this).removeAttr('href') + $(this).attr('clicktracking', 'off').attr('href', globalUnsubscribeLink) _this.logger?.info(`Global Unsubscribe link replaced`) _this.statsClient?.incr('replaced_global_unsubscribe_link', 1) } @@ -378,7 +380,8 @@ export class SendEmailPerformer extends MessageSendPerformer _this.logger?.info(`Preferences link removed from the html body - ${spaceId}`) _this.statsClient?.incr('removed_preferences_link', 1) } else { - $(this).attr('href', preferencesLink) + $(this).removeAttr('href') + $(this).attr('clicktracking', 'off').attr('href', preferencesLink) _this.logger?.info(`Preferences link replaced - ${spaceId}`) _this.statsClient?.incr('replaced_preferences_link', 1) } From 449e287bf9627449b6215e76c160f6bf20b5b96a Mon Sep 17 00:00:00 2001 From: Matt Shwery Date: Mon, 4 Dec 2023 10:40:48 -0800 Subject: [PATCH 28/34] Switch to main url (#1748) * update old cdn domain to current one! * undo auto-formatted changes --- packages/browser-destinations/destinations/koala/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/browser-destinations/destinations/koala/src/index.ts b/packages/browser-destinations/destinations/koala/src/index.ts index 1d164cb2a6..c991ac6db1 100644 --- a/packages/browser-destinations/destinations/koala/src/index.ts +++ b/packages/browser-destinations/destinations/koala/src/index.ts @@ -30,7 +30,7 @@ export const destination: BrowserDestinationDefinition = { initialize: async ({ settings, analytics }, deps) => { initScript() - await deps.loadScript(`https://cdn.koala.live/v1/${settings.project_slug}/umd.js`) + await deps.loadScript(`https://cdn.getkoala.com/v1/${settings.project_slug}/umd.js`) const ko = await window.KoalaSDK.load({ project: settings.project_slug, From d736b5ffc03741fb6018ca07f2205d6c6013d2e6 Mon Sep 17 00:00:00 2001 From: Ankit Gupta <139338151+AnkitSegment@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:39:15 +0530 Subject: [PATCH 29/34] [STRATCONN] 3413 Added External Id in AddProfileToList and RemoveProfileFromList (#1754) * STRATCONN-3413 Added External Id in add Profile to list and remove profile to list * modified test cases * Update index.test.ts Tiktok-audience * Change in commit msg --- .../__snapshots__/snapshot.test.ts.snap | 1 + .../addProfileToList/__tests__/index.test.ts | 91 +++++++++++++++++-- .../addProfileToList/generated-types.ts | 6 +- .../klaviyo/addProfileToList/index.ts | 13 +-- .../src/destinations/klaviyo/functions.ts | 21 ++++- .../src/destinations/klaviyo/properties.ts | 10 +- .../__tests__/index.test.ts | 51 ++++++++++- .../removeProfileFromList/generated-types.ts | 6 +- .../klaviyo/removeProfileFromList/index.ts | 15 +-- .../src/destinations/klaviyo/types.ts | 10 +- 10 files changed, 191 insertions(+), 33 deletions(-) diff --git a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap index b3e4f1a064..2f1f93455e 100644 --- a/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/klaviyo/__tests__/__snapshots__/snapshot.test.ts.snap @@ -5,6 +5,7 @@ Object { "data": Object { "attributes": Object { "email": "mudwoz@zo.ad", + "external_id": "E3nNk", }, "type": "profile", }, diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts index 90e1d4a27a..c2ff188962 100644 --- a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/__tests__/index.test.ts @@ -32,18 +32,18 @@ const profileData = { } describe('Add List To Profile', () => { - it('should throw error if no list_id/email is provided', async () => { + it('should throw error if no email or External Id is provided', async () => { const event = createTestEvent({ type: 'track', properties: {} }) - await expect(testDestination.testAction('addProfileToList', { event, settings })).rejects.toThrowError( - AggregateAjvError - ) + await expect( + testDestination.testAction('addProfileToList', { event, settings, useDefaultMappings: true }) + ).rejects.toThrowError(AggregateAjvError) }) - it('should add profile to list if successful', async () => { + it('should add profile to list if successful with email only', async () => { nock(`${API_URL}`) .post('/profiles/', profileData) .reply(200, { @@ -69,7 +69,7 @@ describe('Add List To Profile', () => { } }) const mapping = { - external_id: listId, + list_id: listId, email: { '@path': '$.traits.email' } @@ -79,6 +79,83 @@ describe('Add List To Profile', () => { ).resolves.not.toThrowError() }) + it('should add profile to list if successful with external id only', async () => { + nock(`${API_URL}`) + .post('/profiles/', { data: { type: 'profile', attributes: { external_id: 'testing_123' } } }) + .reply(200, { + data: { + id: 'XYZABC' + } + }) + + nock(`${API_URL}/lists/${listId}`) + .post('/relationships/profiles/', requestBody) + .reply( + 200, + JSON.stringify({ + content: requestBody + }) + ) + + const event = createTestEvent({ + type: 'track', + userId: '123', + properties: { + external_id: 'testing_123' + } + }) + const mapping = { + list_id: listId, + external_id: 'testing_123' + } + + await expect( + testDestination.testAction('addProfileToList', { event, mapping, settings }) + ).resolves.not.toThrowError() + }) + + it('should add profile to list if successful with both email and external id', async () => { + nock(`${API_URL}`) + .post('/profiles/', { + data: { type: 'profile', attributes: { email: 'demo@segment.com', external_id: 'testing_123' } } + }) + .reply(200, { + data: { + id: 'XYZABC' + } + }) + + nock(`${API_URL}/lists/${listId}`) + .post('/relationships/profiles/', requestBody) + .reply( + 200, + JSON.stringify({ + content: requestBody + }) + ) + + const event = createTestEvent({ + type: 'track', + userId: '123', + properties: { + external_id: 'testing_123' + }, + traits: { + email: 'demo@segment.com' + } + }) + const mapping = { + list_id: listId, + external_id: 'testing_123', + email: { + '@path': '$.traits.email' + } + } + + await expect( + testDestination.testAction('addProfileToList', { event, mapping, settings }) + ).resolves.not.toThrowError() + }) it('should add to list if profile is already created', async () => { nock(`${API_URL}`) .post('/profiles/', profileData) @@ -109,7 +186,7 @@ describe('Add List To Profile', () => { } }) const mapping = { - external_id: listId, + list_id: listId, email: { '@path': '$.traits.email' } diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts index dac9c71807..1a6f0cb2d5 100644 --- a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/generated-types.ts @@ -8,5 +8,9 @@ export interface Payload { /** * 'Insert the ID of the default list that you'd like to subscribe users to when you call .identify().' */ - external_id: string + list_id: string + /** + * A unique identifier used by customers to associate Klaviyo profiles with profiles in an external system. One of External ID and Email required. + */ + external_id?: string } diff --git a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts index 6167cc3ff0..39a4ce4c02 100644 --- a/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/addProfileToList/index.ts @@ -2,7 +2,7 @@ import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' import type { Settings } from '../generated-types' import { Payload } from './generated-types' import { createProfile, addProfileToList } from '../functions' -import { email, external_id } from '../properties' +import { email, list_id, external_id } from '../properties' const action: ActionDefinition = { title: 'Add Profile To List', @@ -10,15 +10,16 @@ const action: ActionDefinition = { defaultSubscription: 'event = "Audience Entered"', fields: { email: { ...email }, + list_id: { ...list_id }, external_id: { ...external_id } }, perform: async (request, { payload }) => { - const { email, external_id } = payload - if (!email) { - throw new PayloadValidationError('Missing Email') + const { email, list_id, external_id } = payload + if (!email && !external_id) { + throw new PayloadValidationError('One of Email or External Id is required') } - const profileId = await createProfile(request, email) - return await addProfileToList(request, profileId, external_id) + const profileId = await createProfile(request, email, external_id) + return await addProfileToList(request, profileId, list_id) } } diff --git a/packages/destination-actions/src/destinations/klaviyo/functions.ts b/packages/destination-actions/src/destinations/klaviyo/functions.ts index fe35b6b39a..aa22b28301 100644 --- a/packages/destination-actions/src/destinations/klaviyo/functions.ts +++ b/packages/destination-actions/src/destinations/klaviyo/functions.ts @@ -59,20 +59,33 @@ export async function removeProfileFromList(request: RequestClient, id: string, return list } -export async function getProfile(request: RequestClient, email: string) { - const profile = await request(`${API_URL}/profiles/?filter=equals(email,"${email}")`, { +export async function getProfile(request: RequestClient, email: string | undefined, external_id: string | undefined) { + let filter + if (external_id) { + filter = `external_id,"${external_id}"` + } + if (email) { + filter = `email,"${email}"` + } + // If both email and external_id are provided. Email will take precedence. + const profile = await request(`${API_URL}/profiles/?filter=equals(${filter})`, { method: 'GET' }) return profile.json() } -export async function createProfile(request: RequestClient, email: string) { +export async function createProfile( + request: RequestClient, + email: string | undefined, + external_id: string | undefined +) { try { const profileData: ProfileData = { data: { type: 'profile', attributes: { - email + email, + external_id } } } diff --git a/packages/destination-actions/src/destinations/klaviyo/properties.ts b/packages/destination-actions/src/destinations/klaviyo/properties.ts index ae15fdb5b5..1cc99b9e59 100644 --- a/packages/destination-actions/src/destinations/klaviyo/properties.ts +++ b/packages/destination-actions/src/destinations/klaviyo/properties.ts @@ -1,7 +1,7 @@ import { InputField } from '@segment/actions-core/destination-kit/types' -export const external_id: InputField = { - label: 'External Id', +export const list_id: InputField = { + label: 'List Id', description: `'Insert the ID of the default list that you'd like to subscribe users to when you call .identify().'`, type: 'string', default: { @@ -20,3 +20,9 @@ export const email: InputField = { }, readOnly: true } + +export const external_id: InputField = { + label: 'External ID', + description: `A unique identifier used by customers to associate Klaviyo profiles with profiles in an external system. One of External ID and Email required.`, + type: 'string' +} diff --git a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts index 3117aff251..fe68b64c59 100644 --- a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/__tests__/index.test.ts @@ -25,7 +25,7 @@ describe('Remove List from Profile', () => { ) }) - it('should remove profile from list if successful', async () => { + it('should remove profile from list if successful with email address only', async () => { const requestBody = { data: [ { @@ -69,4 +69,53 @@ describe('Remove List from Profile', () => { testDestination.testAction('removeProfileFromList', { event, settings, useDefaultMappings: true }) ).resolves.not.toThrowError() }) + + it('should remove profile from list if successful with External Id only', async () => { + const requestBody = { + data: [ + { + type: 'profile', + id: 'XYZABC' + } + ] + } + + const external_id = 'testing_123' + nock(`${API_URL}/profiles`) + .get(`/?filter=equals(external_id,"${external_id}")`) + .reply(200, { + data: [{ id: 'XYZABC' }] + }) + + nock(`${API_URL}/lists/${listId}`) + .delete('/relationships/profiles/', requestBody) + .reply(200, { + data: [ + { + id: 'XYZABC' + } + ] + }) + + const event = createTestEvent({ + type: 'track', + userId: '123', + context: { + personas: { + external_audience_id: listId + } + }, + properties: { + external_id: 'testing_123' + } + }) + const mapping = { + list_id: listId, + external_id: 'testing_123' + } + + await expect( + testDestination.testAction('removeProfileFromList', { event, mapping, settings }) + ).resolves.not.toThrowError() + }) }) diff --git a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts index dac9c71807..7087bb8dad 100644 --- a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/generated-types.ts @@ -5,8 +5,12 @@ export interface Payload { * The user's email to send to Klavio. */ email?: string + /** + * A unique identifier used by customers to associate Klaviyo profiles with profiles in an external system. One of External ID and Email required. + */ + external_id?: string /** * 'Insert the ID of the default list that you'd like to subscribe users to when you call .identify().' */ - external_id: string + list_id: string } diff --git a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts index 12a96756e4..34e956a915 100644 --- a/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts +++ b/packages/destination-actions/src/destinations/klaviyo/removeProfileFromList/index.ts @@ -3,7 +3,7 @@ import type { Settings } from '../generated-types' import { Payload } from './generated-types' import { getProfile, removeProfileFromList } from '../functions' -import { email, external_id } from '../properties' +import { email, list_id, external_id } from '../properties' const action: ActionDefinition = { title: 'Remove profile from list', @@ -11,17 +11,18 @@ const action: ActionDefinition = { defaultSubscription: 'event = "Audience Exited"', fields: { email: { ...email }, - external_id: { ...external_id } + external_id: { ...external_id }, + list_id: { ...list_id } }, perform: async (request, { payload }) => { - const { email, external_id } = payload - if (!email) { - throw new PayloadValidationError('Missing Email') + const { email, list_id, external_id } = payload + if (!email && !external_id) { + throw new PayloadValidationError('Missing Email or External Id') } - const profileData = await getProfile(request, email) + const profileData = await getProfile(request, email, external_id) const v = profileData.data if (v && v.length !== 0) { - return await removeProfileFromList(request, v[0].id, external_id) + return await removeProfileFromList(request, v[0].id, list_id) } } } diff --git a/packages/destination-actions/src/destinations/klaviyo/types.ts b/packages/destination-actions/src/destinations/klaviyo/types.ts index 9eca558977..5932f124fc 100644 --- a/packages/destination-actions/src/destinations/klaviyo/types.ts +++ b/packages/destination-actions/src/destinations/klaviyo/types.ts @@ -63,10 +63,12 @@ export interface EventData { } export interface listData { - data: { - type: string - id?: string - }[] + data: listAttributes[] +} + +export interface listAttributes { + type: string + id?: string } export interface ListIdResponse { From 404965dd093a7a3df64d91e17b63ac84ab5afcd1 Mon Sep 17 00:00:00 2001 From: Elena Date: Tue, 5 Dec 2023 02:22:25 -0800 Subject: [PATCH 30/34] error if space_id includes special chars (#1758) --- .../src/destinations/yahoo-audiences/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/yahoo-audiences/index.ts b/packages/destination-actions/src/destinations/yahoo-audiences/index.ts index 5b6e4f1f54..39552ff9db 100644 --- a/packages/destination-actions/src/destinations/yahoo-audiences/index.ts +++ b/packages/destination-actions/src/destinations/yahoo-audiences/index.ts @@ -54,7 +54,12 @@ const destination: AudienceDestinationDefinition = { } const body_form_data = gen_customer_taxonomy_payload(settings) - // The last 2 params are undefined because we don't have statsContext.statsClient and statsContext.tags in testAuthentication() + // Throw error if engage_space_id contains special characters other then [a-zA-Z0-9] and "_" (underscore) + // This is to prevent the user from creating a customer node with a name that is not allowed by Yahoo + if (!/^[A-Za-z0-9_]+$/.test(settings.engage_space_id)) { + throw new IntegrationError('Invalid Engage Space Id setting', 'INVALID_GLOBAL_SETTING', 400) + } + // The last 2 params are undefined because statsContext.statsClient and statsContext.tags are not available testAuthentication() return await update_taxonomy('', tx_creds, request, body_form_data, undefined, undefined) }, refreshAccessToken: async (request, { auth }) => { @@ -108,6 +113,10 @@ const destination: AudienceDestinationDefinition = { const audienceSettings = createAudienceInput.audienceSettings // @ts-ignore type is not defined, and we will define it later const personas = audienceSettings.personas as PersonasSettings + if (!personas) { + throw new IntegrationError('Missing computation parameters: Id and Key', 'MISSING_REQUIRED_FIELD', 400) + } + const engage_space_id = createAudienceInput.settings?.engage_space_id const audience_id = personas.computation_id const audience_key = personas.computation_key From 091b3ddfa058d48d303fc6b314f8eb00e3919810 Mon Sep 17 00:00:00 2001 From: hvardhan-unth <117922634+hvardhan-unth@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:25:27 +0530 Subject: [PATCH 31/34] LinkedIn Conversions API destination + core changes supporting conversion rule hook (#1716) * Scaffold new Linkedin Conversion destination * Updated unit tests * Added stream conversion event action * Updated the fields * Updated the fields * add new field campaign list * Update unit test case * Made chnages in ad account Id * Update snapshot.test.ts * Update snapshot.test.ts * Update index.test.ts * Remove lint changes * updated the snapshot test cases * Resolved Comments * Updated the error message * Removed the console log * Removes top level conversionId in favor of using the conversion ID saved by the conversion rule hook * Temporary: Disables conversionId dynamic field. Will move this to a dynamic hook input field * Core changes needed to pull in output from onMappingSave hook into perform block * Fixes broken yarn build * Breaks user object into two separate userIds and userInfo objects * Replaces usages of .user field with .userIds and .userInfo fields * Implementation of a dynamic hook input field. Includes: Core Local Server LinkedIn specific implementation * Added function to change timestamp to millisecond format * Fixes broken build by casting hookInput dynamic function to boolean when present, used when constructing schema. * Updates unit test helper methods to support dynamic hook input fields * Updates LinkedIn unit tests * Updates api unit test * v3.89.1-linkedin.0 * v3.230.0-linkedin.0 * Moves creationRule hook logic to api.ts. If existing rule is passed in, checks that it exists and returns it's metadata, else errors * Tested and working return of existing conversion rule * Choices array for userIds.idType * Yarn install * Removes custom core/actions package.json versions. Removes stray moengage edit. Build passing * Removes yarn.lock update * Adds some explanation comments to executeDynamicField method Renames upsertConversionRule to createConversionRule, since it's not actually an upsert operation --------- Co-authored-by: Harsh Vardhan Co-authored-by: Nick Aguilar --- packages/cli/src/lib/server.ts | 43 +- packages/core/src/create-test-integration.ts | 13 +- packages/core/src/destination-kit/action.ts | 54 ++- packages/core/src/destination-kit/index.ts | 18 +- packages/core/src/destination-kit/types.ts | 2 +- .../__snapshots__/snapshot.test.ts.snap | 14 +- .../__tests__/index.test.ts | 21 +- .../__tests__/snapshot.test.ts | 38 +- .../linkedin-conversions/api/api.test.ts | 222 +++++++++++ .../linkedin-conversions/api/index.ts | 248 +++++++++++- .../linkedin-conversions/constants.ts | 7 + .../linkedin-conversions/index.ts | 3 +- .../__snapshots__/snapshot.test.ts.snap | 14 +- .../streamConversion/__tests__/index.test.ts | 370 +++++++++++++++++- .../__tests__/snapshot.test.ts | 38 +- .../streamConversion/generated-types.ts | 48 +++ .../streamConversion/index.ts | 254 +++++++++--- .../linkedin-conversions/types.ts | 77 ++++ .../moengage/identifyUser/index.ts | 2 +- 19 files changed, 1376 insertions(+), 110 deletions(-) create mode 100644 packages/destination-actions/src/destinations/linkedin-conversions/api/api.test.ts diff --git a/packages/cli/src/lib/server.ts b/packages/cli/src/lib/server.ts index 9d1f6c5333..ff75d3a220 100644 --- a/packages/cli/src/lib/server.ts +++ b/packages/cli/src/lib/server.ts @@ -20,7 +20,8 @@ import { AggregateAjvError } from '../../../ajv-human-errors/src/aggregate-ajv-e import { ActionHookType, ActionHookResponse, - AudienceDestinationConfigurationWithCreateGet + AudienceDestinationConfigurationWithCreateGet, + RequestFn } from '@segment/actions-core/destination-kit' interface ResponseError extends Error { status?: number @@ -377,6 +378,46 @@ function setupRoutes(def: DestinationDefinition | null): void { } }) ) + + const inputFields = definition.hooks?.[hookName as ActionHookType]?.inputFields + const dynamicInputs: Record = {} + if (inputFields) { + for (const fieldKey in inputFields) { + const field = inputFields[fieldKey] + if (field.dynamic && typeof field.dynamic === 'function') { + dynamicInputs[fieldKey] = field.dynamic + } + } + } + + for (const fieldKey in dynamicInputs) { + router.post( + `/${actionSlug}/hooks/${hookName}/dynamic/${fieldKey}`, + asyncHandler(async (req: express.Request, res: express.Response) => { + try { + const data = { + settings: req.body.settings || {}, + payload: req.body.payload || {}, + page: req.body.page || 1, + auth: req.body.auth || {}, + audienceSettings: req.body.audienceSettings || {}, + hookInputs: req.body.hookInputs || {} + } + const action = destination.actions[actionSlug] + const dynamicFn = dynamicInputs[fieldKey] as RequestFn + const result = await action.executeDynamicField(fieldKey, data, dynamicFn) + + if (result.error) { + throw result.error + } + + return res.status(200).json(result) + } catch (err) { + return res.status(500).json([err]) + } + }) + ) + } } } } diff --git a/packages/core/src/create-test-integration.ts b/packages/core/src/create-test-integration.ts index b41ab2cf3c..c47afeea69 100644 --- a/packages/core/src/create-test-integration.ts +++ b/packages/core/src/create-test-integration.ts @@ -1,13 +1,13 @@ import { createTestEvent } from './create-test-event' import { StateContext, Destination, TransactionContext } from './destination-kit' import { mapValues } from './map-values' -import type { DestinationDefinition, StatsContext, Logger, DataFeedCache } from './destination-kit' +import type { DestinationDefinition, StatsContext, Logger, DataFeedCache, RequestFn } from './destination-kit' import type { JSONObject } from './json-object' import type { SegmentEvent } from './segment-event' import { AuthTokens } from './destination-kit/parse-settings' import { Features } from './mapping-kit' import { ExecuteDynamicFieldInput } from './destination-kit/action' -import { Result } from './destination-kit/types' +import { DynamicFieldResponse, Result } from './destination-kit/types' // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {} @@ -56,8 +56,13 @@ class TestDestination extends Destination) { - return await super.executeDynamicField(action, fieldKey, data) + async testDynamicField( + action: string, + fieldKey: string, + data: ExecuteDynamicFieldInput, + dynamicFn?: RequestFn + ) { + return await super.executeDynamicField(action, fieldKey, data, dynamicFn) } /** Testing method that runs an action e2e while allowing slightly more flexible inputs */ diff --git a/packages/core/src/destination-kit/action.ts b/packages/core/src/destination-kit/action.ts index 7cc92032d3..43233e9925 100644 --- a/packages/core/src/destination-kit/action.ts +++ b/packages/core/src/destination-kit/action.ts @@ -125,7 +125,12 @@ export interface ActionHookDefinition< /** A description of what this hook does. */ description: string /** The configuration fields that are used when executing the hook. The values will be provided by users in the app. */ - inputFields?: Record + inputFields?: Record< + string, + Omit & { + dynamic?: RequestFn + } + > /** The shape of the return from performHook. These values will be available in the generated-types: Payload for use in perform() */ outputTypes?: Record /** The operation to perform when this hook is triggered. */ @@ -203,7 +208,24 @@ export class Action = {} + for (const key in hook.inputFields) { + const field = hook.inputFields[key] + + if (field.dynamic) { + castedInputFields[key] = { + ...field, + dynamic: true + } + } else { + castedInputFields[key] = { + ...field, + dynamic: false + } + } + } + + this.hookSchemas[hookName] = fieldsToJsonSchema(castedInputFields) } } } @@ -227,6 +249,17 @@ export class Action + data: ExecuteDynamicFieldInput, + /** + * The dynamicFn argument is optional since it is only used by dynamic hook input fields. (For now) + */ + dynamicFn?: RequestFn ): Promise { - const fn = this.definition.dynamicFields?.[field] + let fn + if (dynamicFn && typeof dynamicFn === 'function') { + fn = dynamicFn + } else { + fn = this.definition.dynamicFields?.[field] + } + if (typeof fn !== 'function') { return Promise.resolve({ choices: [], diff --git a/packages/core/src/destination-kit/index.ts b/packages/core/src/destination-kit/index.ts index 1d22dbfeba..1518cc004d 100644 --- a/packages/core/src/destination-kit/index.ts +++ b/packages/core/src/destination-kit/index.ts @@ -19,7 +19,15 @@ import { fieldsToJsonSchema, MinimalInputField } from './fields-to-jsonschema' import createRequestClient, { RequestClient, ResponseError } from '../create-request-client' import { validateSchema } from '../schema-validation' import type { ModifiedResponse } from '../types' -import type { GlobalSetting, RequestExtension, ExecuteInput, Result, Deletion, DeletionPayload } from './types' +import type { + GlobalSetting, + RequestExtension, + ExecuteInput, + Result, + Deletion, + DeletionPayload, + DynamicFieldResponse +} from './types' import type { AllRequestOptions } from '../request-client' import { ErrorCodes, IntegrationError, InvalidAuthenticationError } from '../errors' import { AuthTokens, getAuthData, getOAuth2Data, updateOAuthSettings } from './parse-settings' @@ -613,14 +621,18 @@ export class Destination { public async executeDynamicField( actionSlug: string, fieldKey: string, - data: ExecuteDynamicFieldInput + data: ExecuteDynamicFieldInput, + /** + * The dynamicFn argument is optional since it is only used by dynamic hook input fields. (For now) + */ + dynamicFn?: RequestFn ) { const action = this.actions[actionSlug] if (!action) { return [] } - return action.executeDynamicField(fieldKey, data) + return action.executeDynamicField(fieldKey, data, dynamicFn) } private async onSubscription( diff --git a/packages/core/src/destination-kit/types.ts b/packages/core/src/destination-kit/types.ts index 200b6c6056..fd7d3bb48e 100644 --- a/packages/core/src/destination-kit/types.ts +++ b/packages/core/src/destination-kit/types.ts @@ -34,7 +34,7 @@ export interface ExecuteInput< /** Inputs into an actions hook performHook method */ hookInputs?: ActionHookInputs /** Stored outputs from an invokation of an actions hook */ - hookOutputs?: Record + hookOutputs?: Partial> /** The page used in dynamic field requests */ page?: string /** The data needed in OAuth requests */ diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/__snapshots__/snapshot.test.ts.snap index c480333df6..4ad2aef649 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for actions-linkedin-conversions destination: streamConversion action - all fields 1`] = `Object {}`; +exports[`Testing snapshot for actions-linkedin-conversions destination: streamConversion action - all fields 1`] = ` +Object { + "campaign": "urn:li:sponsoredCampaign:Fuj)Xdj", + "conversion": "urn:lla:llaPartnerConversion:1234", +} +`; -exports[`Testing snapshot for actions-linkedin-conversions destination: streamConversion action - required fields 1`] = `Object {}`; +exports[`Testing snapshot for actions-linkedin-conversions destination: streamConversion action - required fields 1`] = ` +Object { + "campaign": "urn:li:sponsoredCampaign:Fuj)Xdj", + "conversion": "urn:lla:llaPartnerConversion:1234", +} +`; diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/index.test.ts b/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/index.test.ts index a096339b17..2427142934 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/index.test.ts @@ -5,7 +5,7 @@ import { BASE_URL } from '../constants' const testDestination = createTestIntegration(Definition) -const settings = { +const validSettings = { oauth: { access_token: '123', refresh_token: '123' @@ -14,19 +14,20 @@ const settings = { describe('Linkedin Conversions Api', () => { describe('testAuthentication', () => { - it('should validate authentication inputs', async () => { + it('should not throw an error if all the appropriate credentials are available', async () => { const mockProfileResponse = { - id: '456' + id: '123' } // Validate that the user exists in LinkedIn. nock(`${BASE_URL}/me`).get(/.*/).reply(200, mockProfileResponse) - await expect(testDestination.testAuthentication(settings)).resolves.not.toThrowError() + await expect(testDestination.testAuthentication(validSettings)).resolves.not.toThrowError() }) it('should throw an error if the user has not completed the oauth flow', async () => { - await expect(testDestination.testAuthentication({})).rejects.toThrowError( + const invalidOauth = {} + await expect(testDestination.testAuthentication(invalidOauth)).rejects.toThrowError( 'Credentials are invalid: Please authenticate via Oauth before enabling the destination.' ) }) @@ -34,17 +35,9 @@ describe('Linkedin Conversions Api', () => { it('should throw an error if the oauth token is invalid', async () => { nock(`${BASE_URL}/me`).get(/.*/).reply(401) - await expect(testDestination.testAuthentication(settings)).rejects.toThrowError( + await expect(testDestination.testAuthentication(validSettings)).rejects.toThrowError( 'Credentials are invalid: Invalid LinkedIn Oauth access token. Please reauthenticate to retrieve a valid access token before enabling the destination.' ) }) - - it('should throw the raw error from LinkedIn if the error is not handled elsewhere in the `testAuthentication` method', async () => { - nock(`${BASE_URL}/me`).get(/.*/).reply(500) - - await expect(testDestination.testAuthentication(settings)).rejects.toThrowError( - 'Credentials are invalid: 500 Internal Server Error' - ) - }) }) }) diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/snapshot.test.ts index f49cb2be92..8884f87cab 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/__tests__/snapshot.test.ts @@ -17,13 +17,30 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => { nock(/.*/).persist().post(/.*/).reply(200) nock(/.*/).persist().put(/.*/).reply(200) + eventData.userIds = [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + } + ] + + eventData.conversionHappenedAt = '1698764171467' + const event = createTestEvent({ properties: eventData }) const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: event.properties, + mapping: { + ...event.properties, + onMappingSave: { + inputs: {}, + outputs: { + id: '1234' + } + } + }, settings: settingsData, auth: undefined }) @@ -51,13 +68,30 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => { nock(/.*/).persist().post(/.*/).reply(200) nock(/.*/).persist().put(/.*/).reply(200) + eventData.userIds = [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + } + ] + + eventData.conversionHappenedAt = '1698764171467' + const event = createTestEvent({ properties: eventData }) const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: event.properties, + mapping: { + ...event.properties, + onMappingSave: { + inputs: {}, + outputs: { + id: '1234' + } + } + }, settings: settingsData, auth: undefined }) diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/api/api.test.ts b/packages/destination-actions/src/destinations/linkedin-conversions/api/api.test.ts new file mode 100644 index 0000000000..d07a279f4a --- /dev/null +++ b/packages/destination-actions/src/destinations/linkedin-conversions/api/api.test.ts @@ -0,0 +1,222 @@ +import nock from 'nock' +import createRequestClient from '../../../../../core/src/create-request-client' +import { LinkedInConversions } from '../api' +import { BASE_URL } from '../constants' + +const requestClient = createRequestClient() + +describe('LinkedIn Conversions', () => { + describe('dynamicFields', () => { + const linkedIn: LinkedInConversions = new LinkedInConversions(requestClient) + + it('should fetch a list of ad accounts, with their names', async () => { + nock(`${BASE_URL}`) + .get(`/adAccountUsers`) + .query({ q: 'authenticatedUser' }) + .reply(200, { + elements: [ + { + account: 'urn:li:sponsoredAccount:516413367', + changeAuditStamps: { + created: { + actor: 'urn:li:unknown:0', + time: 1500331577000 + }, + lastModified: { + actor: 'urn:li:unknown:0', + time: 1505328748000 + } + }, + role: 'ACCOUNT_BILLING_ADMIN', + user: 'urn:li:person:K1RwyVNukt', + version: { + versionTag: '89' + } + }, + { + account: 'urn:li:sponsoredAccount:516880883', + changeAuditStamps: { + created: { + actor: 'urn:li:unknown:0', + time: 1505326590000 + }, + lastModified: { + actor: 'urn:li:unknown:0', + time: 1505326615000 + } + }, + role: 'ACCOUNT_BILLING_ADMIN', + user: 'urn:li:person:K1RwyVNukt', + version: { + versionTag: '3' + } + } + ], + paging: { + count: 2, + links: [], + start: 0, + total: 2 + } + }) + + const getAdAccountsRes = await linkedIn.getAdAccounts() + expect(getAdAccountsRes).toEqual({ + choices: [ + { + label: 'urn:li:person:K1RwyVNukt', + value: 'urn:li:sponsoredAccount:516413367' + }, + { + label: 'urn:li:person:K1RwyVNukt', + value: 'urn:li:sponsoredAccount:516880883' + } + ] + }) + }) + + it('should fetch a list of conversion rules', async () => { + const payload = { + adAccountId: '123456' + } + nock(`${BASE_URL}`) + .get(`/conversions`) + .query({ q: 'account', account: payload.adAccountId }) + .reply(200, { + elements: [ + { + postClickAttributionWindowSize: 30, + viewThroughAttributionWindowSize: 7, + created: 1563230311551, + type: 'LEAD', + enabled: true, + name: 'Conversion API Segment 2', + lastModified: 1563230311551, + id: 104012, + attributionType: 'LAST_TOUCH_BY_CAMPAIGN', + conversionMethod: 'CONVERSIONS_API', + account: 'urn:li:sponsoredAccount:51234560' + }, + { + postClickAttributionWindowSize: 30, + viewThroughAttributionWindowSize: 7, + created: 1563230255308, + type: 'PURCHASE', + enabled: true, + name: 'Conversion API Segment 3', + lastModified: 1563230265652, + id: 104004, + attributionType: 'LAST_TOUCH_BY_CAMPAIGN', + conversionMethod: 'CONVERSIONS_API', + account: 'urn:li:sponsoredAccount:51234560' + } + ] + }) + + const getConversionRulesListRes = await linkedIn.getConversionRulesList(payload.adAccountId) + expect(getConversionRulesListRes).toEqual({ + choices: [ + { + label: 'Conversion API Segment 2', + value: 104012 + }, + { + label: 'Conversion API Segment 3', + value: 104004 + } + ] + }) + }) + + it('should fetch a list of campaigns', async () => { + const payload = { + adAccountId: '123456' + } + nock(`${BASE_URL}`) + .get(`/adAccounts/${payload.adAccountId}/adCampaigns?q=search&search=(status:(values:List(ACTIVE)))`) + .reply(200, { + paging: { + start: 0, + count: 10, + links: [], + total: 1 + }, + elements: [ + { + test: false, + storyDeliveryEnabled: false, + format: 'TEXT_AD', + targetingCriteria: { + include: { + and: [ + { + or: { + 'urn:li:adTargetingFacet:locations': ['urn:li:geo:90000084'] + } + }, + { + or: { + 'urn:li:adTargetingFacet:interfaceLocales': ['urn:li:locale:en_US'] + } + } + ] + } + }, + servingStatuses: ['ACCOUNT_SERVING_HOLD'], + locale: { + country: 'US', + language: 'en' + }, + type: 'TEXT_AD', + version: { + versionTag: '11' + }, + objectiveType: 'WEBSITE_TRAFFIC', + associatedEntity: 'urn:li:organization:2425698', + optimizationTargetType: 'NONE', + runSchedule: { + start: 1498178362345 + }, + changeAuditStamps: { + created: { + actor: 'urn:li:unknown:0', + time: 1498178304000 + }, + lastModified: { + actor: 'urn:li:unknown:0', + time: 1698494362000 + } + }, + campaignGroup: 'urn:li:sponsoredCampaignGroup:600360846', + dailyBudget: { + currencyCode: 'USD', + amount: '25' + }, + costType: 'CPC', + creativeSelection: 'OPTIMIZED', + unitCost: { + currencyCode: 'USD', + amount: '8.19' + }, + name: 'Test', + offsiteDeliveryEnabled: false, + id: 125868226, + audienceExpansionEnabled: true, + account: 'urn:li:sponsoredAccount:507525021', + status: 'ACTIVE' + } + ] + }) + + const getCampaignsListRes = await linkedIn.getCampaignsList(payload.adAccountId) + expect(getCampaignsListRes).toEqual({ + choices: [ + { + label: 'Test', + value: 125868226 + } + ] + }) + }) + }) +}) diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/api/index.ts b/packages/destination-actions/src/destinations/linkedin-conversions/api/index.ts index 8b39484285..61097c9ae0 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/api/index.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/api/index.ts @@ -1,12 +1,25 @@ -import type { RequestClient, ModifiedResponse } from '@segment/actions-core' +import type { RequestClient, ModifiedResponse, DynamicFieldResponse, ActionHookResponse } from '@segment/actions-core' import { BASE_URL } from '../constants' -import type { ProfileAPIResponse } from '../types' - +import type { + ProfileAPIResponse, + GetAdAccountsAPIResponse, + Accounts, + AccountsErrorInfo, + GetConversionListAPIResponse, + Conversions, + GetCampaignsListAPIResponse, + Campaigns, + ConversionRuleCreationResponse, + GetConversionRuleResponse +} from '../types' +import type { Payload, HookBundle } from '../streamConversion/generated-types' export class LinkedInConversions { request: RequestClient + conversionRuleId?: string - constructor(request: RequestClient) { + constructor(request: RequestClient, conversionRuleId?: string) { this.request = request + this.conversionRuleId = conversionRuleId } async getProfile(): Promise> { @@ -14,4 +27,231 @@ export class LinkedInConversions { method: 'GET' }) } + + createConversionRule = async ( + payload: Payload, + hookInputs: HookBundle['onMappingSave']['inputs'] + ): Promise> => { + if (hookInputs?.conversionRuleId) { + try { + const { data } = await this.request( + `${BASE_URL}/conversions/${this.conversionRuleId}`, + { + method: 'get', + searchParams: { + account: payload?.adAccountId + } + } + ) + + return { + successMessage: `Using existing Conversion Rule: ${hookInputs.conversionRuleId} `, + savedData: { + id: hookInputs.conversionRuleId, + name: data.name || `No name returned for rule: ${hookInputs.conversionRuleId}`, + conversionType: data.type || `No type returned for rule: ${hookInputs.conversionRuleId}` + } + } + } catch (e) { + return { + error: { + message: `Failed to verify conversion rule: ${(e as { message: string })?.message ?? JSON.stringify(e)}`, + code: 'CONVERSION_RULE_VERIFICATION_FAILURE' + } + } + } + } + + try { + const { data } = await this.request(`${BASE_URL}/conversions`, { + method: 'post', + json: { + name: hookInputs?.name, + account: payload?.adAccountId, + conversionMethod: 'CONVERSIONS_API', + postClickAttributionWindowSize: 30, + viewThroughAttributionWindowSize: 7, + attributionType: hookInputs?.attribution_type, + type: hookInputs?.conversionType + } + }) + + return { + successMessage: `Conversion rule ${data.id} created successfully!`, + savedData: { + id: data.id, + name: data.name, + conversionType: data.type + } + } + } catch (e) { + return { + error: { + message: `Failed to create conversion rule: ${(e as { message: string })?.message ?? JSON.stringify(e)}`, + code: 'CONVERSION_RULE_CREATION_FAILURE' + } + } + } + } + + getAdAccounts = async (): Promise => { + try { + const response: Array = [] + const result = await this.request(`${BASE_URL}/adAccountUsers`, { + method: 'GET', + searchParams: { + q: 'authenticatedUser' + } + }) + + result.data.elements.forEach((item) => { + response.push(item) + }) + + const choices = response?.map((item) => { + return { + label: item.user, + value: item.account + } + }) + + return { + choices + } + } catch (err) { + return { + choices: [], + error: { + message: + (err as AccountsErrorInfo).response?.data?.message ?? 'An error occurred while fetching ad accounts.', + code: (err as AccountsErrorInfo).response?.data?.code?.toString() ?? 'FETCH_AD_ACCOUNTS_ERROR' + } + } + } + } + + getConversionRulesList = async (adAccountId: string): Promise => { + if (!adAccountId || !adAccountId.length) { + return { + choices: [], + error: { + message: 'Please select Ad Account first to get list of Conversion Rules.', + code: 'FIELD_NOT_SELECTED' + } + } + } + + try { + const response: Array = [] + const result = await this.request(`${BASE_URL}/conversions`, { + method: 'GET', + searchParams: { + q: 'account', + account: adAccountId + } + }) + + result.data.elements.forEach((item) => { + response.push(item) + }) + + const choices = response?.map((item) => { + return { + label: item.name, + value: item.id + } + }) + + return { + choices + } + } catch (err) { + return { + choices: [], + error: { + message: + (err as AccountsErrorInfo).response?.data?.message ?? 'An error occurred while fetching conversion rules.', + code: (err as AccountsErrorInfo).response?.data?.code?.toString() ?? 'FETCH_CONVERSIONS_ERROR' + } + } + } + } + + getCampaignsList = async (adAccountUrn: string): Promise => { + const parts = adAccountUrn.split(':') + const adAccountId = parts.pop() + + if (!adAccountId || !adAccountId.length) { + return { + choices: [], + error: { + message: 'Please select Ad Account first to get list of Conversion Rules.', + code: 'FIELD_NOT_SELECTED' + } + } + } + + try { + const response: Array = [] + const result = await this.request( + `${BASE_URL}/adAccounts/${adAccountId}/adCampaigns?q=search&search=(status:(values:List(ACTIVE)))`, + { + method: 'GET' + } + ) + + result.data.elements.forEach((item) => { + response.push(item) + }) + + const choices = response?.map((item) => { + return { + label: item.name, + value: item.id + } + }) + + return { + choices + } + } catch (err) { + return { + choices: [], + error: { + message: + (err as AccountsErrorInfo).response?.data?.message ?? 'An error occurred while fetching conversion rules.', + code: (err as AccountsErrorInfo).response?.data?.code?.toString() ?? 'FETCH_CONVERSIONS_ERROR' + } + } + } + } + + async streamConversionEvent(payload: Payload, conversionTime: number): Promise { + return this.request(`${BASE_URL}/conversionEvents`, { + method: 'POST', + json: { + conversion: `urn:lla:llaPartnerConversion:${this.conversionRuleId}`, + conversionHappenedAt: conversionTime, + conversionValue: payload.conversionValue, + eventId: payload.eventId, + user: { + userIds: payload.userIds, + userInfo: payload.userInfo + } + } + }) + } + + async associateCampignToConversion(payload: Payload): Promise { + return this.request( + `${BASE_URL}/campaignConversions/(campaign:urn%3Ali%3AsponsoredCampaign%3A${payload.campaignId},conversion:urn%3Alla%3AllaPartnerConversion%3A${this.conversionRuleId})`, + { + method: 'PUT', + body: JSON.stringify({ + campaign: `urn:li:sponsoredCampaign:${payload.campaignId}`, + conversion: `urn:lla:llaPartnerConversion:${this.conversionRuleId}` + }) + } + ) + } } diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/constants.ts b/packages/destination-actions/src/destinations/linkedin-conversions/constants.ts index e2b8090047..2fbf05603f 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/constants.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/constants.ts @@ -1,3 +1,10 @@ export const LINKEDIN_API_VERSION = '202309' export const BASE_URL = 'https://api.linkedin.com/rest' export const LINKEDIN_SOURCE_PLATFORM = 'SEGMENT' + +export const SUPPORTED_ID_TYPE = [ + 'SHA256_EMAIL', + 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + 'ACXIOM_ID', + 'ORACLE_MOAT_ID' +] diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/index.ts b/packages/destination-actions/src/destinations/linkedin-conversions/index.ts index 5cf2aad008..e72eaa9e2a 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/index.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/index.ts @@ -73,7 +73,8 @@ const destination: DestinationDefinition = { return { headers: { authorization: `Bearer ${auth?.accessToken}`, - 'LinkedIn-Version': LINKEDIN_API_VERSION + 'LinkedIn-Version': LINKEDIN_API_VERSION, + 'X-Restli-Protocol-Version': `2.0.0` } } }, diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/__snapshots__/snapshot.test.ts.snap index daa078d95f..9922decfbb 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,5 +1,15 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for LinkedinConversions's streamConversion destination action: all fields 1`] = `Object {}`; +exports[`Testing snapshot for LinkedinConversions's streamConversion destination action: all fields 1`] = ` +Object { + "campaign": "urn:li:sponsoredCampaign:RK^DrO", + "conversion": "urn:lla:llaPartnerConversion:1234", +} +`; -exports[`Testing snapshot for LinkedinConversions's streamConversion destination action: required fields 1`] = `Object {}`; +exports[`Testing snapshot for LinkedinConversions's streamConversion destination action: required fields 1`] = ` +Object { + "campaign": "urn:li:sponsoredCampaign:RK^DrO", + "conversion": "urn:lla:llaPartnerConversion:1234", +} +`; diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/index.test.ts b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/index.test.ts index 1534e8be68..80d51299b8 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/index.test.ts @@ -1,5 +1,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration } from '@segment/actions-core' +import { DynamicFieldResponse } from '@segment/actions-core' +import { BASE_URL } from '../../constants' import Destination from '../../index' const testDestination = createTestIntegration(Destination) @@ -7,20 +9,376 @@ const testDestination = createTestIntegration(Destination) const event = createTestEvent({ event: 'Example Event', type: 'track', + timestamp: '1695884800000', context: { traits: { - email: 'testing@testing.com' + email: 'testing@testing.com', + adAccountId: '12345', + campaignId: '56789', + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'df5gf5-gh6t7-ph4j7h-fgf6n1' + } + ], + userInfo: { + firstName: 'mike', + lastName: 'smith', + title: 'software engineer', + companyName: 'microsoft', + countryCode: 'US' + } + } } } }) +const settings = {} + describe('LinkedinConversions.streamConversion', () => { - //This is an example unit test case, needs to update after developing streamConversion action - it('A sample unit case', async () => { - nock('https://example.com').post('/').reply(200, {}) + it('should successfully send the event', async () => { + const associateCampignToConversion = { + campaign: 'urn:li:sponsoredCampaign:123456`', + conversion: 'urn:lla:llaPartnerConversion:789123' + } + + const payload = { + campaignId: 123456, + conversionId: 789123 + } + + const streamConversionEvent = { + conversion: `urn:lla:llaPartnerConversion:${payload.conversionId}`, + conversionHappenedAt: 1698764171467, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'df5gf5-gh6t7-ph4j7h-fgf6n1' + } + ], + userInfo: { + firstName: 'mike', + lastName: 'smith', + title: 'software engineer', + companyName: 'microsoft', + countryCode: 'US' + } + } + } + + nock( + `${BASE_URL}/campaignConversions/(campaign:urn%3Ali%3AsponsoredCampaign%3A${payload.campaignId},conversion:urn%3Alla%3AllaPartnerConversion%3A${payload.conversionId})` + ) + .post(/.*/, associateCampignToConversion) + .reply(204) + nock(`${BASE_URL}/conversionEvents`).post(/.*/, streamConversionEvent).reply(201) + + await expect( + testDestination.testAction('streamConversion', { + event, + settings, + mapping: { + adAccountId: { + '@path': '$.context.traits.adAccountId' + }, + user: { + '@path': '$.context.traits.user' + }, + campaignId: { + '@path': '$.context.traits.campaignId' + }, + conversionHappenedAt: { + '@path': '$.timestamp' + }, + onMappingSave: { + inputs: {}, + outputs: { + id: payload.conversionId + } + } + } + }) + ).resolves.not.toThrowError() + }) + + it('should throw an error if timestamp is not within the past 90 days', async () => { + event.timestamp = '50000000000' + + await expect( + testDestination.testAction('streamConversion', { + event, + settings, + mapping: { + adAccountId: { + '@path': '$.context.traits.adAccountId' + }, + user: { + '@path': '$.context.traits.user' + }, + campaignId: { + '@path': '$.context.traits.campaignId' + }, + conversionHappenedAt: { + '@path': '$.timestamp' + } + } + }) + ).rejects.toThrowError('Timestamp should be within the past 90 days.') + }) + + it('should throw an error if Either userIds array or userInfo with firstName and lastName is not present.', async () => { + const event = createTestEvent({ + event: 'Example Event', + type: 'track', + timestamp: '1695884800000', + context: { + traits: { + email: 'testing@testing.com', + adAccountId: '12345', + campaignId: '56789', + userIds: [], + userInfo: { + title: 'software engineer', + companyName: 'microsoft', + countryCode: 'US' + } + } + } + }) + + await expect( + testDestination.testAction('streamConversion', { + event, + settings, + mapping: { + adAccountId: { + '@path': '$.context.traits.adAccountId' + }, + userIds: { + '@path': '$.context.traits.userIds' + }, + userInfo: { + '@path': '$.context.traits.userInfo' + }, + campaignId: { + '@path': '$.context.traits.campaignId' + }, + conversionHappenedAt: { + '@path': '$.timestamp' + }, + onMappingSave: { + inputs: {}, + outputs: { + id: '123' + } + } + } + }) + ).rejects.toThrowError('Either userIds array or userInfo with firstName and lastName should be present.') + }) +}) + +describe('LinkedinConversions.dynamicField', () => { + it('conversionId: should give error if adAccountId is not provided', async () => { + const settings = {} + + const payload = { + adAccountId: '' + } + + const dynamicFn = + testDestination.actions.streamConversion.definition.hooks?.onMappingSave?.inputFields?.conversionRuleId.dynamic + const responses = (await testDestination.testDynamicField( + 'streamConversion', + 'conversionId', + { + settings, + payload + }, + dynamicFn + )) as DynamicFieldResponse + + expect(responses).toMatchObject({ + choices: [], + error: { + message: 'Please select Ad Account first to get list of Conversion Rules.', + code: 'FIELD_NOT_SELECTED' + } + }) + }) + + it('campaignId: should give error if adAccountId is not provided', async () => { + const settings = {} + + const payload = { + adAccountId: '' + } + const responses = (await testDestination.testDynamicField('streamConversion', 'campaignId', { + settings, + payload + })) as DynamicFieldResponse + + expect(responses).toMatchObject({ + choices: [], + error: { + message: 'Please select Ad Account first to get list of Conversion Rules.', + code: 'FIELD_NOT_SELECTED' + } + }) + }) +}) + +describe('LinkedinConversions.timestamp', () => { + it('should convert a human readable date to a unix timestamp', async () => { + event.timestamp = '2023-11-01T12:12:12.125Z' + + const associateCampignToConversion = { + campaign: 'urn:li:sponsoredCampaign:123456`', + conversion: 'urn:lla:llaPartnerConversion:789123' + } + + const payload = { + campaignId: 123456, + conversionId: 789123 + } + + const streamConversionEvent = { + conversion: `urn:lla:llaPartnerConversion:${payload.conversionId}`, + conversionHappenedAt: 1698840732125, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'df5gf5-gh6t7-ph4j7h-fgf6n1' + } + ], + userInfo: { + firstName: 'mike', + lastName: 'smith', + title: 'software engineer', + companyName: 'microsoft', + countryCode: 'US' + } + } + } + + nock( + `${BASE_URL}/campaignConversions/(campaign:urn%3Ali%3AsponsoredCampaign%3A${payload.campaignId},conversion:urn%3Alla%3AllaPartnerConversion%3A${payload.conversionId})` + ) + .post(/.*/, associateCampignToConversion) + .reply(204) + nock(`${BASE_URL}/conversionEvents`).post(/.*/, streamConversionEvent).reply(201) + + await expect( + testDestination.testAction('streamConversion', { + event, + settings, + mapping: { + adAccountId: { + '@path': '$.context.traits.adAccountId' + }, + user: { + '@path': '$.context.traits.user' + }, + campaignId: { + '@path': '$.context.traits.campaignId' + }, + conversionHappenedAt: { + '@path': '$.timestamp' + }, + onMappingSave: { + inputs: {}, + outputs: { + id: payload.conversionId + } + } + } + }) + ).resolves.not.toThrowError() + }) + + it('should convert a string unix timestamp to a number', async () => { + event.timestamp = '1698840732125' + + const associateCampignToConversion = { + campaign: 'urn:li:sponsoredCampaign:123456`', + conversion: 'urn:lla:llaPartnerConversion:789123' + } + + const payload = { + campaignId: 123456, + conversionId: 789123 + } + + const streamConversionEvent = { + conversion: `urn:lla:llaPartnerConversion:${payload.conversionId}`, + conversionHappenedAt: 1698840732125, + user: { + userIds: [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + }, + { + idType: 'LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID', + idValue: 'df5gf5-gh6t7-ph4j7h-fgf6n1' + } + ], + userInfo: { + firstName: 'mike', + lastName: 'smith', + title: 'software engineer', + companyName: 'microsoft', + countryCode: 'US' + } + } + } + + nock( + `${BASE_URL}/campaignConversions/(campaign:urn%3Ali%3AsponsoredCampaign%3A${payload.campaignId},conversion:urn%3Alla%3AllaPartnerConversion%3A${payload.conversionId})` + ) + .post(/.*/, associateCampignToConversion) + .reply(204) + nock(`${BASE_URL}/conversionEvents`).post(/.*/, streamConversionEvent).reply(201) + await expect( - testDestination.testAction('sampleEvent', { - event + testDestination.testAction('streamConversion', { + event, + settings, + mapping: { + adAccountId: { + '@path': '$.context.traits.adAccountId' + }, + user: { + '@path': '$.context.traits.user' + }, + campaignId: { + '@path': '$.context.traits.campaignId' + }, + conversionHappenedAt: { + '@path': '$.timestamp' + }, + onMappingSave: { + inputs: {}, + outputs: { + id: payload.conversionId + } + } + } }) ).resolves.not.toThrowError() }) diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/snapshot.test.ts b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/snapshot.test.ts index b74061f058..6097e411ce 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/snapshot.test.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/__tests__/snapshot.test.ts @@ -17,13 +17,30 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac nock(/.*/).persist().post(/.*/).reply(200) nock(/.*/).persist().put(/.*/).reply(200) + eventData.userIds = [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + } + ] + + eventData.conversionHappenedAt = '1698764171467' + const event = createTestEvent({ properties: eventData }) const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: event.properties, + mapping: { + ...event.properties, + onMappingSave: { + inputs: {}, + outputs: { + id: '1234' + } + } + }, settings: settingsData, auth: undefined }) @@ -50,13 +67,30 @@ describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination ac nock(/.*/).persist().post(/.*/).reply(200) nock(/.*/).persist().put(/.*/).reply(200) + eventData.userIds = [ + { + idType: 'SHA256_EMAIL', + idValue: 'bad8677b6c86f5d308ee82786c183482a5995f066694246c58c4df37b0cc41f1' + } + ] + + eventData.conversionHappenedAt = '1698764171467' + const event = createTestEvent({ properties: eventData }) const responses = await testDestination.testAction(actionSlug, { event: event, - mapping: event.properties, + mapping: { + ...event.properties, + onMappingSave: { + inputs: {}, + outputs: { + id: '1234' + } + } + }, settings: settingsData, auth: undefined }) diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/generated-types.ts b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/generated-types.ts index 99cb7b413d..a0385d5b27 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/generated-types.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/generated-types.ts @@ -5,6 +5,54 @@ export interface Payload { * A dynamic field dropdown which fetches all adAccounts. */ adAccountId: string + /** + * Epoch timestamp in milliseconds at which the conversion event happened. If your source records conversion timestamps in second, insert 000 at the end to transform it to milliseconds. + */ + conversionHappenedAt: string + /** + * The monetary value for this conversion. Example: {“currencyCode”: “USD”, “amount”: “50.0”}. + */ + conversionValue?: { + /** + * ISO format + */ + currencyCode: string + /** + * Value of the conversion in decimal string. Can be dynamically set up or have a fixed value. + */ + amount: string + } + /** + * Will be used for deduplication in future. + */ + eventId?: string + /** + * Either userIds or userInfo is required. List of one or more identifiers to match the conversion user with objects containing "idType" and "idValue". + */ + userIds?: { + /** + * Valid values are: SHA256_EMAIL, LINKEDIN_FIRST_PARTY_ADS_TRACKING_UUID, ACXIOM_ID, ORACLE_MOAT_ID + */ + idType: string + /** + * The value of the identifier. + */ + idValue: string + }[] + /** + * Object containing additional fields for user matching. + */ + userInfo?: { + firstName?: string + lastName?: string + companyName?: string + title?: string + countryCode?: string + } + /** + * A dynamic field dropdown which fetches all active campaigns. + */ + campaignId: string } // Generated bundle for hooks. DO NOT MODIFY IT BY HAND. diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/index.ts b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/index.ts index 8e39c08a4a..0091afa426 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/index.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/streamConversion/index.ts @@ -1,29 +1,14 @@ import type { ActionDefinition } from '@segment/actions-core' +import { PayloadValidationError } from '@segment/actions-core' import type { Settings } from '../generated-types' +import { LinkedInConversions } from '../api' +import { SUPPORTED_ID_TYPE } from '../constants' import type { Payload, HookBundle } from './generated-types' -interface ConversionRuleCreationResponse { - id: string - name: string - type: string -} - -interface LinkedInError { - message: string -} - const action: ActionDefinition = { title: 'Stream Conversion Event', description: 'Directly streams conversion events to a specific conversion rule.', - fields: { - adAccountId: { - label: 'Ad Account', - description: 'A dynamic field dropdown which fetches all adAccounts.', - type: 'string', - required: true, - dynamic: true - } - }, + defaultSubscription: 'type = "track"', hooks: { onMappingSave: { label: 'Create a Conversion Rule', @@ -40,7 +25,11 @@ const action: ActionDefinition = { label: 'Existing Conversion Rule ID', description: 'The ID of an existing conversion rule to stream events to. If defined, we will not create a new conversion rule.', - required: false + required: false, + dynamic: async (request, { payload }) => { + const linkedIn = new LinkedInConversions(request) + return linkedIn.getConversionRulesList(payload.adAccountId) + } }, name: { type: 'string', @@ -96,55 +85,196 @@ const action: ActionDefinition = { } }, performHook: async (request, { payload, hookInputs }) => { - if (hookInputs?.conversionRuleId) { - return { - successMessage: `Using existing Conversion Rule: ${hookInputs.conversionRuleId} `, - savedData: { - id: hookInputs.conversionRuleId, - name: hookInputs.name, - conversionType: hookInputs.conversionType - } - } + const linkedIn = new LinkedInConversions(request, hookInputs?.conversionRuleId) + return await linkedIn.createConversionRule(payload, hookInputs) + } + } + }, + fields: { + adAccountId: { + label: 'Ad Account', + description: 'A dynamic field dropdown which fetches all adAccounts.', + type: 'string', + required: true, + dynamic: true + }, + conversionHappenedAt: { + label: 'Timestamp', + description: + 'Epoch timestamp in milliseconds at which the conversion event happened. If your source records conversion timestamps in second, insert 000 at the end to transform it to milliseconds.', + type: 'string', + required: true, + default: { + '@path': '$.timestamp' + } + }, + conversionValue: { + label: 'Conversion Value', + description: 'The monetary value for this conversion. Example: {“currencyCode”: “USD”, “amount”: “50.0”}.', + type: 'object', + required: false, + properties: { + currencyCode: { + label: 'Currency Code', + type: 'string', + required: true, + description: 'ISO format' + }, + amount: { + label: 'Amount', + type: 'string', + required: true, + description: 'Value of the conversion in decimal string. Can be dynamically set up or have a fixed value.' } - - try { - const { data } = await request('https://api.linkedin.com/rest/conversions', { - method: 'post', - json: { - name: hookInputs?.name, - account: payload?.adAccountId, - conversionMethod: 'CONVERSIONS_API', - postClickAttributionWindowSize: 30, - viewThroughAttributionWindowSize: 7, - attributionType: hookInputs?.attribution_type, - type: hookInputs?.conversionType - } - }) - - return { - successMessage: `Conversion rule ${data.id} created successfully!`, - savedData: { - id: data.id, - name: data.name, - conversionType: data.type - } - } - } catch (e) { - return { - errorMessage: `Failed to create conversion rule: ${(e as LinkedInError)?.message ?? JSON.stringify(e)}` - } + } + }, + eventId: { + label: 'Event ID', + description: 'Will be used for deduplication in future.', + type: 'string', + required: false, + default: { + '@path': '$.messageId' + } + }, + userIds: { + label: 'User Ids', + description: + 'Either userIds or userInfo is required. List of one or more identifiers to match the conversion user with objects containing "idType" and "idValue".', + type: 'object', + multiple: true, + properties: { + idType: { + label: 'ID Type', + description: `Valid values are: ${SUPPORTED_ID_TYPE.join(', ')}`, + choices: SUPPORTED_ID_TYPE, + type: 'string', + required: true + }, + idValue: { + label: 'ID Value', + description: 'The value of the identifier.', + type: 'string', + required: true + } + } + }, + userInfo: { + label: 'User Info', + description: 'Object containing additional fields for user matching.', + type: 'object', + required: false, + properties: { + firstName: { + label: 'First Name', + type: 'string', + required: false + }, + lastName: { + label: 'Last Name', + type: 'string', + required: false + }, + companyName: { + label: 'Company Name', + type: 'string', + required: false + }, + title: { + label: 'Title', + type: 'string', + required: false + }, + countryCode: { + label: 'Country Code', + type: 'string', + required: false } } + }, + campaignId: { + label: 'Campaign', + type: 'string', + required: true, + dynamic: true, + description: 'A dynamic field dropdown which fetches all active campaigns.' } }, - perform: (request, data) => { - return request('https://example.com', { - method: 'post', - json: { - conversion: data.hookOutputs?.onMappingSave?.id - } + dynamicFields: { + adAccountId: async (request) => { + const linkedIn = new LinkedInConversions(request) + return linkedIn.getAdAccounts() + }, + campaignId: async (request, { payload }) => { + const linkedIn = new LinkedInConversions(request) + return linkedIn.getCampaignsList(payload.adAccountId) + } + }, + perform: async (request, { payload, hookOutputs }) => { + const conversionTime = isNotEpochTimestampInMilliseconds(payload.conversionHappenedAt) + ? convertToEpochMillis(payload.conversionHappenedAt) + : Number(payload.conversionHappenedAt) + validate(payload, conversionTime) + + let conversionRuleId = '' + if (hookOutputs?.onMappingSave?.outputs?.id) { + conversionRuleId = hookOutputs?.onMappingSave.outputs?.id + } + + if (!conversionRuleId) { + throw new PayloadValidationError('Conversion Rule ID is required.') + } + + const linkedinApiClient: LinkedInConversions = new LinkedInConversions(request, conversionRuleId) + try { + await linkedinApiClient.associateCampignToConversion(payload) + return linkedinApiClient.streamConversionEvent(payload, conversionTime) + } catch (error) { + return error + } + } +} + +function validate(payload: Payload, conversionTime: number) { + // Check if the timestamp is within the past 90 days + const ninetyDaysAgo = Date.now() - 90 * 24 * 60 * 60 * 1000 + if (conversionTime < ninetyDaysAgo) { + throw new PayloadValidationError('Timestamp should be within the past 90 days.') + } + + if ( + payload.userIds && + Array.isArray(payload.userIds) && + payload.userIds?.length === 0 && + (!payload.userInfo || !(payload.userInfo.firstName && payload.userInfo.lastName)) + ) { + throw new PayloadValidationError('Either userIds array or userInfo with firstName and lastName should be present.') + } else if (payload.userIds && payload.userIds.length !== 0) { + const isValidUserIds = payload.userIds.every((obj) => { + return SUPPORTED_ID_TYPE.includes(obj.idType) }) + + if (!isValidUserIds) { + throw new PayloadValidationError(`Invalid idType in userIds field. Allowed idType will be: ${SUPPORTED_ID_TYPE}`) + } } } +function isNotEpochTimestampInMilliseconds(timestamp: string) { + if (typeof timestamp === 'string' && !isNaN(Number(timestamp))) { + const convertedTimestamp = Number(timestamp) + const startDate = new Date('1970-01-01T00:00:00Z').getTime() + const endDate = new Date('2100-01-01T00:00:00Z').getTime() + if (Number.isSafeInteger(convertedTimestamp) && convertedTimestamp >= startDate && convertedTimestamp <= endDate) { + return false + } + } + return true +} + +function convertToEpochMillis(timestamp: string) { + const date = new Date(timestamp) + return date.getTime() +} + export default action diff --git a/packages/destination-actions/src/destinations/linkedin-conversions/types.ts b/packages/destination-actions/src/destinations/linkedin-conversions/types.ts index 89977695a6..a2fd8893c8 100644 --- a/packages/destination-actions/src/destinations/linkedin-conversions/types.ts +++ b/packages/destination-actions/src/destinations/linkedin-conversions/types.ts @@ -27,3 +27,80 @@ export class LinkedInRefreshTokenError extends HTTPError { } } } + +export interface GetAdAccountsAPIResponse { + paging: { + count: number + links: Array + start: number + total: number + } + elements: [Accounts] +} + +export interface Accounts { + account: string + changeAuditStamps: object + role: string + user: string + version: object +} + +export interface AccountsErrorInfo { + response: { + data: { + message?: string + code?: string + } + } +} + +export interface GetConversionListAPIResponse { + paging: { + count: number + links: Array + start: number + total: number + } + elements: [Conversions] +} + +export interface Conversions { + name: string + id: string +} + +export interface GetCampaignsListAPIResponse { + paging: { + count: number + links: Array + start: number + total: number + } + elements: [Campaigns] +} + +export interface Campaigns { + name: string + id: string +} + +export interface ConversionRuleCreationResponse { + id: string + name: string + type: string +} + +/** + * The shape of the response from LinkedIn when fetching a conversion rule by id. + * Not all properties in this type are used, but they are included if needed in the future. + */ +export interface GetConversionRuleResponse { + conversionMethod?: string + type?: string + enabled?: boolean + name?: string + id?: string + attributionType?: string + account?: string +} diff --git a/packages/destination-actions/src/destinations/moengage/identifyUser/index.ts b/packages/destination-actions/src/destinations/moengage/identifyUser/index.ts index 4d09b9ae87..0fc44337c8 100644 --- a/packages/destination-actions/src/destinations/moengage/identifyUser/index.ts +++ b/packages/destination-actions/src/destinations/moengage/identifyUser/index.ts @@ -115,4 +115,4 @@ const action: ActionDefinition = { } } -export default action \ No newline at end of file +export default action From 924571d4659dd138f87a082a3cb667548d2fe5d6 Mon Sep 17 00:00:00 2001 From: Innovative-GauravKochar <117165746+Innovative-GauravKochar@users.noreply.github.com> Date: Tue, 5 Dec 2023 18:27:31 +0530 Subject: [PATCH 32/34] update goofle api from v 13 to v15 in google enhanced conversion (#1746) Co-authored-by: Gaurav Kochar --- .../src/destinations/google-enhanced-conversions/functions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts b/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts index e4f1f90641..5969aba58c 100644 --- a/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts +++ b/packages/destination-actions/src/destinations/google-enhanced-conversions/functions.ts @@ -19,7 +19,7 @@ import { Features } from '@segment/actions-core/mapping-kit' import { fullFormats } from 'ajv-formats/dist/formats' export const API_VERSION = 'v13' -export const CANARY_API_VERSION = 'v13' +export const CANARY_API_VERSION = 'v15' export const FLAGON_NAME = 'google-enhanced-canary-version' export function formatCustomVariables( From 016f87c6894515719c44c5410955568b9d5577b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sezer=20G=C3=BCven?= <70070685+esezerguven@users.noreply.github.com> Date: Tue, 5 Dec 2023 15:58:57 +0300 Subject: [PATCH 33/34] Feature/SD-97275 Append Arrays (#1752) * SD-97275 | Append arrays * SD-97275 | Append arrays * SD-97275 | Append arrays --- .../insider-audiences/insider-helpers.ts | 5 +++++ .../insiderAudiences/__tests__/index.test.ts | 15 ++++++++++++++ .../insiderAudiences/generated-types.ts | 4 ++++ .../insiderAudiences/index.ts | 6 ++++++ .../__snapshots__/snapshot.test.ts.snap | 20 +++++++++++++++++++ .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../cartViewedEvent/generated-types.ts | 4 ++++ .../insider/cartViewedEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../insider/checkoutEvent/generated-types.ts | 4 ++++ .../insider/checkoutEvent/index.ts | 4 +++- .../destinations/insider/insider-helpers.ts | 12 +++++++++-- .../insider/insider-properties.ts | 7 +++++++ .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../orderCompletedEvent/generated-types.ts | 4 ++++ .../insider/orderCompletedEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../productAddedEvent/generated-types.ts | 4 ++++ .../insider/productAddedEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../productListViewedEvent/generated-types.ts | 4 ++++ .../insider/productListViewedEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../productRemovedEvent/generated-types.ts | 4 ++++ .../insider/productRemovedEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../productViewedEvent/generated-types.ts | 4 ++++ .../insider/productViewedEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 4 ++-- .../insider/trackEvent/generated-types.ts | 4 ++++ .../destinations/insider/trackEvent/index.ts | 4 +++- .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../updateUserProfile/generated-types.ts | 4 ++++ .../insider/updateUserProfile/index.ts | 2 ++ .../__snapshots__/snapshot.test.ts.snap | 2 ++ .../userRegisteredEvent/generated-types.ts | 4 ++++ .../insider/userRegisteredEvent/index.ts | 4 +++- 37 files changed, 156 insertions(+), 13 deletions(-) diff --git a/packages/destination-actions/src/destinations/insider-audiences/insider-helpers.ts b/packages/destination-actions/src/destinations/insider-audiences/insider-helpers.ts index 7c1882ccce..4e6640193e 100644 --- a/packages/destination-actions/src/destinations/insider-audiences/insider-helpers.ts +++ b/packages/destination-actions/src/destinations/insider-audiences/insider-helpers.ts @@ -31,6 +31,7 @@ const computedTraitsPayloadForIdentifyCall = function ( attributes } ], + not_append: !data.append_arrays, platform: 'segment' } @@ -55,6 +56,7 @@ const computedTraitsPayloadForTrackCall = function ( events } ], + not_append: !data.append_arrays, platform: 'segment' } @@ -86,6 +88,7 @@ const computedAudiencesPayloadForIdentifyCall = function ( attributes } ], + not_append: !data.append_arrays, platform: 'segment' } @@ -110,6 +113,7 @@ const computedAudiencePayloadForTrackCall = function ( events } ], + not_append: !data.append_arrays, platform: 'segment' } @@ -135,6 +139,7 @@ const deleteAttributePartial = function (data: Payload) { } } ], + not_append: !data.append_arrays, platform: 'segment' } } diff --git a/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/__tests__/index.test.ts b/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/__tests__/index.test.ts index 3870bf316b..b498ef0168 100644 --- a/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/__tests__/index.test.ts @@ -15,6 +15,7 @@ describe('processPayload', () => { event_name: 'segment event', segment_computation_action: 'trait', custom_audience_name: 'example_audience', + append_arrays: false, traits_or_props: { example_audience: true, email: 'example@example.com' @@ -47,6 +48,7 @@ describe('processPayload', () => { ] } ], + not_append: true, platform: 'segment' } }) @@ -57,6 +59,7 @@ describe('processPayload', () => { { custom_audience_name: 'num_link_clicked_l_60_d', segment_computation_action: 'trait', + append_arrays: false, email: 'example@example.com', phone: '1234567890', anonymous_id: '123', @@ -90,6 +93,7 @@ describe('processPayload', () => { } } ], + not_append: true, platform: 'segment' } }) @@ -100,6 +104,7 @@ describe('processPayload', () => { { custom_audience_name: 'example_audience', segment_computation_action: 'trait', + append_arrays: false, email: 'example@example.com', phone: '1234567890', traits_or_props: { @@ -135,6 +140,7 @@ describe('processPayload', () => { ] } ], + not_append: true, platform: 'segment' } }) @@ -145,6 +151,7 @@ describe('processPayload', () => { { custom_audience_name: 'demo_squarkai', segment_computation_action: 'audience', + append_arrays: false, email: 'example@example.com', traits_or_props: { demo_squarkai: true, @@ -172,6 +179,7 @@ describe('processPayload', () => { } } ], + not_append: true, platform: 'segment' } }) @@ -182,6 +190,7 @@ describe('processPayload', () => { { custom_audience_name: 'demo_squarkai', segment_computation_action: 'audience', + append_arrays: false, email: 'example@example.com', event_name: 'Segment Event', traits_or_props: { @@ -216,6 +225,7 @@ describe('processPayload', () => { ] } ], + not_append: true, platform: 'segment' } }) @@ -227,6 +237,7 @@ describe('processPayload', () => { event_type: 'identify', segment_computation_action: 'audience', custom_audience_name: 'example_audience', + append_arrays: false, traits_or_props: { example_audience: false, email: 'example@example.com' @@ -254,6 +265,7 @@ describe('processPayload', () => { } } ], + not_append: true, platform: 'segment' } }) @@ -265,6 +277,7 @@ describe('processPayload', () => { event_type: 'identify', segment_computation_action: 'invalid', custom_audience_name: 'example_trait', + append_arrays: false, traits_or_props: { example_trait: 'example_value', email: 'example@example.com' @@ -287,6 +300,7 @@ describe('processPayload', () => { event_type: 'invalid', segment_computation_action: 'audience', custom_audience_name: 'invalid_event_test', + append_arrays: false, traits_or_props: { example_trait: 'example_value', email: 'example@example.com' @@ -310,6 +324,7 @@ describe('processPayload', () => { event_type: 'invalid', segment_computation_action: 'trait', custom_audience_name: 'invalid_event_test', + append_arrays: false, traits_or_props: { example_trait: 'example_value', email: 'example@example.com' diff --git a/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/generated-types.ts b/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/generated-types.ts index 0e50b35a6d..30c41d67f4 100644 --- a/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * Segment computation class used to determine if action is an 'Engage-Audience' */ segment_computation_action: string + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's email address for including/excluding from custom audience */ diff --git a/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/index.ts b/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/index.ts index 79a22df62f..fdaf8a80a3 100644 --- a/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/index.ts +++ b/packages/destination-actions/src/destinations/insider-audiences/insiderAudiences/index.ts @@ -26,6 +26,12 @@ const action: ActionDefinition = { '@path': '$.context.personas.computation_class' } }, + append_arrays: { + label: 'Append Array Fields', + type: 'boolean', + description: 'If enabled, new data for array fields will be appended to the existing values in Insider.', + default: false + }, email: { label: 'Email', description: "User's email address for including/excluding from custom audience", diff --git a/packages/destination-actions/src/destinations/insider/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/__tests__/__snapshots__/snapshot.test.ts.snap index 22ddbecf73..4b62b4a0e3 100644 --- a/packages/destination-actions/src/destinations/insider/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/__tests__/__snapshots__/snapshot.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "uuid": "0xlI!lem[5]2MJvda", }, + "not_append": true, }, ], } @@ -81,6 +82,7 @@ Object { }, "uuid": "0xlI!lem[5]2MJvda", }, + "not_append": true, }, ], } @@ -139,6 +141,7 @@ Object { }, "uuid": "i2DYKi53!byOli1^))*", }, + "not_append": true, }, ], } @@ -167,6 +170,7 @@ Object { }, "uuid": "i2DYKi53!byOli1^))*", }, + "not_append": true, }, ], } @@ -225,6 +229,7 @@ Object { }, "uuid": "%detmPE)QcMI3#y0Y", }, + "not_append": true, }, ], } @@ -253,6 +258,7 @@ Object { }, "uuid": "%detmPE)QcMI3#y0Y", }, + "not_append": true, }, ], } @@ -311,6 +317,7 @@ Object { }, "uuid": "pVmlekeqp9EloEHBS", }, + "not_append": true, }, ], } @@ -339,6 +346,7 @@ Object { }, "uuid": "pVmlekeqp9EloEHBS", }, + "not_append": true, }, ], } @@ -390,6 +398,7 @@ Object { }, "uuid": "SFVk3AFZB*U7Pg", }, + "not_append": true, }, ], } @@ -418,6 +427,7 @@ Object { }, "uuid": "SFVk3AFZB*U7Pg", }, + "not_append": true, }, ], } @@ -478,6 +488,7 @@ Object { "phone_number": "!Ivq)L", "uuid": "!Ivq)L", }, + "not_append": false, }, ], } @@ -506,6 +517,7 @@ Object { }, "uuid": "!Ivq)L", }, + "not_append": true, }, ], } @@ -566,6 +578,7 @@ Object { "phone_number": "jjiPS4iz", "uuid": "jjiPS4iz", }, + "not_append": false, }, ], } @@ -594,6 +607,7 @@ Object { }, "uuid": "jjiPS4iz", }, + "not_append": true, }, ], } @@ -655,6 +669,7 @@ Object { }, "uuid": "74Eoa(TEWXz$1Kje", }, + "not_append": true, }, ], } @@ -683,6 +698,7 @@ Object { }, "uuid": "74Eoa(TEWXz$1Kje", }, + "not_append": true, }, ], } @@ -720,6 +736,7 @@ Object { "phone_number": "CA9^h[(o", "uuid": "CA9^h[(o", }, + "not_append": false, }, ], } @@ -737,6 +754,7 @@ Object { }, "uuid": "CA9^h[(o", }, + "not_append": true, }, ], } @@ -785,6 +803,7 @@ Object { "phone_number": "xt]Bf", "uuid": "xt]Bf", }, + "not_append": false, }, ], } @@ -813,6 +832,7 @@ Object { }, "uuid": "xt]Bf", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/cartViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/cartViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 9df40a2255..68deecd16e 100644 --- a/packages/destination-actions/src/destinations/insider/cartViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/cartViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "uuid": "uh[f!Dz)ZqkDd$x", }, + "not_append": true, }, ], } @@ -81,6 +82,7 @@ Object { }, "uuid": "uh[f!Dz)ZqkDd$x", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/cartViewedEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/cartViewedEvent/generated-types.ts index 6096c3f9ff..cb41d4c7e3 100644 --- a/packages/destination-actions/src/destinations/insider/cartViewedEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/cartViewedEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/cartViewedEvent/index.ts b/packages/destination-actions/src/destinations/insider/cartViewedEvent/index.ts index ecc6c16a0c..f3152e76b9 100644 --- a/packages/destination-actions/src/destinations/insider/cartViewedEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/cartViewedEvent/index.ts @@ -9,7 +9,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -20,6 +21,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/checkoutEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/checkoutEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 12b5dbd545..2256e09f51 100644 --- a/packages/destination-actions/src/destinations/insider/checkoutEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/checkoutEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -55,6 +55,7 @@ Object { "phone_number": "Q!Q[jv1Wi&s0", "uuid": "Q!Q[jv1Wi&s0", }, + "not_append": false, }, ], } @@ -83,6 +84,7 @@ Object { }, "uuid": "Q!Q[jv1Wi&s0", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/checkoutEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/checkoutEvent/generated-types.ts index 6096c3f9ff..cb41d4c7e3 100644 --- a/packages/destination-actions/src/destinations/insider/checkoutEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/checkoutEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/checkoutEvent/index.ts b/packages/destination-actions/src/destinations/insider/checkoutEvent/index.ts index 38d11d0914..a658d3b30a 100644 --- a/packages/destination-actions/src/destinations/insider/checkoutEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/checkoutEvent/index.ts @@ -9,7 +9,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -20,6 +21,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/insider-helpers.ts b/packages/destination-actions/src/destinations/insider/insider-helpers.ts index f4f68c5474..799f709821 100644 --- a/packages/destination-actions/src/destinations/insider/insider-helpers.ts +++ b/packages/destination-actions/src/destinations/insider/insider-helpers.ts @@ -24,6 +24,7 @@ export interface upsertUserPayload { custom?: object } attributes: { [key: string]: never } + not_append: boolean events: insiderEvent[] } @@ -67,7 +68,8 @@ export function userProfilePayload(data: UserPayload) { whatsapp_optin: data.whatsappOptin, language: data.language?.replace('-', '_'), custom: data.custom - } + }, + not_append: !data.append_arrays } ], platform: 'segment' @@ -252,11 +254,14 @@ export function sendTrackEvent( payload.events.push(event) } + payload.not_append = !data.append_arrays + return { users: [payload], platform: 'segment' } } export function bulkUserProfilePayload(data: UserPayload[]) { const batchPayload = data.map((userPayload) => { + const not_append = !userPayload.append_arrays const identifiers = { uuid: userPayload.uuid, custom: { @@ -306,7 +311,7 @@ export function bulkUserProfilePayload(data: UserPayload[]) { } }) - return { identifiers, attributes } + return { identifiers, attributes, not_append } }) return { users: batchPayload, platform: 'segment' } @@ -395,6 +400,7 @@ export function sendBulkTrackEvents( // @ts-ignore custom: {} }, + not_append: true, events: [] } @@ -497,6 +503,8 @@ export function sendBulkTrackEvents( payload.events.push(event) } + payload.not_append = !data.append_arrays + bulkPayload.push(payload) }) diff --git a/packages/destination-actions/src/destinations/insider/insider-properties.ts b/packages/destination-actions/src/destinations/insider/insider-properties.ts index 298ff34203..38f805d22f 100644 --- a/packages/destination-actions/src/destinations/insider/insider-properties.ts +++ b/packages/destination-actions/src/destinations/insider/insider-properties.ts @@ -133,6 +133,13 @@ export const phone_number_as_identifier: InputField = { default: true } +export const append_arrays: InputField = { + label: 'Append Array Fields', + type: 'boolean', + description: 'If enabled, new data for array fields will be appended to the existing values in Insider.', + default: false +} + export const uuid: InputField = { label: 'UUID', type: 'string', diff --git a/packages/destination-actions/src/destinations/insider/orderCompletedEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/orderCompletedEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 98e8f197b4..b80f54a66e 100644 --- a/packages/destination-actions/src/destinations/insider/orderCompletedEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/orderCompletedEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -55,6 +55,7 @@ Object { "phone_number": "^nSY[JM", "uuid": "^nSY[JM", }, + "not_append": false, }, ], } @@ -83,6 +84,7 @@ Object { }, "uuid": "^nSY[JM", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/orderCompletedEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/orderCompletedEvent/generated-types.ts index 6096c3f9ff..cb41d4c7e3 100644 --- a/packages/destination-actions/src/destinations/insider/orderCompletedEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/orderCompletedEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/orderCompletedEvent/index.ts b/packages/destination-actions/src/destinations/insider/orderCompletedEvent/index.ts index 145a7c294d..6d9befe5fd 100644 --- a/packages/destination-actions/src/destinations/insider/orderCompletedEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/orderCompletedEvent/index.ts @@ -9,7 +9,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -20,6 +21,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/productAddedEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/productAddedEvent/__tests__/__snapshots__/snapshot.test.ts.snap index fa1f1f19ac..5c5279d342 100644 --- a/packages/destination-actions/src/destinations/insider/productAddedEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/productAddedEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "uuid": "G!)xRN2DwZB33yYCIUOK", }, + "not_append": true, }, ], } @@ -81,6 +82,7 @@ Object { }, "uuid": "G!)xRN2DwZB33yYCIUOK", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/productAddedEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/productAddedEvent/generated-types.ts index 1337dee57d..a3ea0a1ca7 100644 --- a/packages/destination-actions/src/destinations/insider/productAddedEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/productAddedEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/productAddedEvent/index.ts b/packages/destination-actions/src/destinations/insider/productAddedEvent/index.ts index 9fd348fc90..0624fba315 100644 --- a/packages/destination-actions/src/destinations/insider/productAddedEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/productAddedEvent/index.ts @@ -8,7 +8,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -19,6 +20,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/productListViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/productListViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap index da17de95c0..c8f8af83fd 100644 --- a/packages/destination-actions/src/destinations/insider/productListViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/productListViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -46,6 +46,7 @@ Object { }, "uuid": "XPOGmR%$*4[TgwzNYN", }, + "not_append": true, }, ], } @@ -74,6 +75,7 @@ Object { }, "uuid": "XPOGmR%$*4[TgwzNYN", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/productListViewedEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/productListViewedEvent/generated-types.ts index 989efc83c3..c6e19b2d44 100644 --- a/packages/destination-actions/src/destinations/insider/productListViewedEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/productListViewedEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/productListViewedEvent/index.ts b/packages/destination-actions/src/destinations/insider/productListViewedEvent/index.ts index 5a5932b440..a969eeb4d4 100644 --- a/packages/destination-actions/src/destinations/insider/productListViewedEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/productListViewedEvent/index.ts @@ -8,7 +8,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -19,6 +20,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/productRemovedEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/productRemovedEvent/__tests__/__snapshots__/snapshot.test.ts.snap index aebf4e6dbe..4be5971449 100644 --- a/packages/destination-actions/src/destinations/insider/productRemovedEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/productRemovedEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "uuid": "Lu!Xj33sF(%SQN", }, + "not_append": true, }, ], } @@ -81,6 +82,7 @@ Object { }, "uuid": "Lu!Xj33sF(%SQN", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/productRemovedEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/productRemovedEvent/generated-types.ts index 1337dee57d..a3ea0a1ca7 100644 --- a/packages/destination-actions/src/destinations/insider/productRemovedEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/productRemovedEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/productRemovedEvent/index.ts b/packages/destination-actions/src/destinations/insider/productRemovedEvent/index.ts index 7731410b85..6dcc77d5ef 100644 --- a/packages/destination-actions/src/destinations/insider/productRemovedEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/productRemovedEvent/index.ts @@ -8,7 +8,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -19,6 +20,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/productViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/productViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 08785fc174..c0e9f452bc 100644 --- a/packages/destination-actions/src/destinations/insider/productViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/productViewedEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -53,6 +53,7 @@ Object { }, "uuid": "y$Fq1#L9R1N]w7PGv", }, + "not_append": true, }, ], } @@ -81,6 +82,7 @@ Object { }, "uuid": "y$Fq1#L9R1N]w7PGv", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/productViewedEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/productViewedEvent/generated-types.ts index 1337dee57d..a3ea0a1ca7 100644 --- a/packages/destination-actions/src/destinations/insider/productViewedEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/productViewedEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/productViewedEvent/index.ts b/packages/destination-actions/src/destinations/insider/productViewedEvent/index.ts index 0c11bf38c5..48c7ad2745 100644 --- a/packages/destination-actions/src/destinations/insider/productViewedEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/productViewedEvent/index.ts @@ -8,7 +8,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -19,6 +20,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, diff --git a/packages/destination-actions/src/destinations/insider/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 940312bc8b..5735360a36 100644 --- a/packages/destination-actions/src/destinations/insider/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/trackEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Testing snapshot for Insider's trackEvent destination action: all fields 1`] = `"{\\"users\\":[{\\"identifiers\\":{\\"uuid\\":\\"Ef*GhBp7kEUO\\",\\"custom\\":{\\"segment_anonymous_id\\":\\"Ef*GhBp7kEUO\\"},\\"email\\":\\"tuoc@giciwco.net\\",\\"phone_number\\":\\"Ef*GhBp7kEUO\\"},\\"attributes\\":{\\"custom\\":{},\\"email\\":\\"tuoc@giciwco.net\\",\\"phone\\":\\"Ef*GhBp7kEUO\\",\\"age\\":-10845372872130.56,\\"birthday\\":\\"Ef*GhBp7kEUO\\",\\"name\\":\\"Ef*GhBp7kEUO\\",\\"gender\\":\\"Ef*GhBp7kEUO\\",\\"surname\\":\\"Ef*GhBp7kEUO\\",\\"app_version\\":\\"Ef*GhBp7kEUO\\",\\"idfa\\":\\"Ef*GhBp7kEUO\\",\\"model\\":\\"Ef*GhBp7kEUO\\",\\"last_ip\\":\\"Ef*GhBp7kEUO\\",\\"city\\":\\"Ef*GhBp7kEUO\\",\\"country\\":\\"Ef*GhBp7kEUO\\",\\"carrier\\":\\"Ef*GhBp7kEUO\\",\\"os_version\\":\\"Ef*GhBp7kEUO\\",\\"platform\\":\\"Ef*GhBp7kEUO\\",\\"timezone\\":\\"Ef*GhBp7kEUO\\",\\"locale\\":\\"Ef*GhBp7kEUO\\"},\\"events\\":[{\\"event_name\\":\\"ef*ghbp7keuo\\",\\"timestamp\\":\\"2021-02-01T00:00:00.000Z\\",\\"event_params\\":{\\"custom\\":{},\\"url\\":\\"Ef*GhBp7kEUO\\",\\"currency\\":\\"LTL\\",\\"product_id\\":\\"Ef*GhBp7kEUO\\",\\"taxonomy\\":[\\"Ef*GhBp7kEUO\\"],\\"name\\":\\"Ef*GhBp7kEUO\\",\\"variant_id\\":-10845372872130.56,\\"unit_sale_price\\":-10845372872130.56,\\"unit_price\\":-10845372872130.56,\\"quantity\\":-1084537287213056,\\"product_image_url\\":\\"Ef*GhBp7kEUO\\",\\"event_group_id\\":\\"Ef*GhBp7kEUO\\",\\"referrer\\":\\"Ef*GhBp7kEUO\\",\\"user_agent\\":\\"Ef*GhBp7kEUO\\"}}]}],\\"platform\\":\\"segment\\"}"`; +exports[`Testing snapshot for Insider's trackEvent destination action: all fields 1`] = `"{\\"users\\":[{\\"identifiers\\":{\\"uuid\\":\\"Ef*GhBp7kEUO\\",\\"custom\\":{\\"segment_anonymous_id\\":\\"Ef*GhBp7kEUO\\"},\\"email\\":\\"tuoc@giciwco.net\\",\\"phone_number\\":\\"Ef*GhBp7kEUO\\"},\\"attributes\\":{\\"custom\\":{},\\"email\\":\\"tuoc@giciwco.net\\",\\"phone\\":\\"Ef*GhBp7kEUO\\",\\"age\\":-10845372872130.56,\\"birthday\\":\\"Ef*GhBp7kEUO\\",\\"name\\":\\"Ef*GhBp7kEUO\\",\\"gender\\":\\"Ef*GhBp7kEUO\\",\\"surname\\":\\"Ef*GhBp7kEUO\\",\\"app_version\\":\\"Ef*GhBp7kEUO\\",\\"idfa\\":\\"Ef*GhBp7kEUO\\",\\"model\\":\\"Ef*GhBp7kEUO\\",\\"last_ip\\":\\"Ef*GhBp7kEUO\\",\\"city\\":\\"Ef*GhBp7kEUO\\",\\"country\\":\\"Ef*GhBp7kEUO\\",\\"carrier\\":\\"Ef*GhBp7kEUO\\",\\"os_version\\":\\"Ef*GhBp7kEUO\\",\\"platform\\":\\"Ef*GhBp7kEUO\\",\\"timezone\\":\\"Ef*GhBp7kEUO\\",\\"locale\\":\\"Ef*GhBp7kEUO\\"},\\"events\\":[{\\"event_name\\":\\"ef*ghbp7keuo\\",\\"timestamp\\":\\"2021-02-01T00:00:00.000Z\\",\\"event_params\\":{\\"custom\\":{},\\"url\\":\\"Ef*GhBp7kEUO\\",\\"currency\\":\\"LTL\\",\\"product_id\\":\\"Ef*GhBp7kEUO\\",\\"taxonomy\\":[\\"Ef*GhBp7kEUO\\"],\\"name\\":\\"Ef*GhBp7kEUO\\",\\"variant_id\\":-10845372872130.56,\\"unit_sale_price\\":-10845372872130.56,\\"unit_price\\":-10845372872130.56,\\"quantity\\":-1084537287213056,\\"product_image_url\\":\\"Ef*GhBp7kEUO\\",\\"event_group_id\\":\\"Ef*GhBp7kEUO\\",\\"referrer\\":\\"Ef*GhBp7kEUO\\",\\"user_agent\\":\\"Ef*GhBp7kEUO\\"}}],\\"not_append\\":false}],\\"platform\\":\\"segment\\"}"`; -exports[`Testing snapshot for Insider's trackEvent destination action: required fields 1`] = `"{\\"users\\":[{\\"identifiers\\":{\\"uuid\\":\\"Ef*GhBp7kEUO\\",\\"custom\\":{\\"segment_anonymous_id\\":\\"Ef*GhBp7kEUO\\"}},\\"attributes\\":{\\"custom\\":{}},\\"events\\":[{\\"event_name\\":\\"ef*ghbp7keuo\\",\\"timestamp\\":\\"2021-02-01T00:00:00.000Z\\",\\"event_params\\":{\\"custom\\":{}}}]}],\\"platform\\":\\"segment\\"}"`; +exports[`Testing snapshot for Insider's trackEvent destination action: required fields 1`] = `"{\\"users\\":[{\\"identifiers\\":{\\"uuid\\":\\"Ef*GhBp7kEUO\\",\\"custom\\":{\\"segment_anonymous_id\\":\\"Ef*GhBp7kEUO\\"}},\\"attributes\\":{\\"custom\\":{}},\\"events\\":[{\\"event_name\\":\\"ef*ghbp7keuo\\",\\"timestamp\\":\\"2021-02-01T00:00:00.000Z\\",\\"event_params\\":{\\"custom\\":{}}}],\\"not_append\\":true}],\\"platform\\":\\"segment\\"}"`; exports[`Testing snapshot for Insider's trackEvent destination action: required fields 2`] = ` Headers { diff --git a/packages/destination-actions/src/destinations/insider/trackEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/trackEvent/generated-types.ts index 6aa1f838a2..450e2d105b 100644 --- a/packages/destination-actions/src/destinations/insider/trackEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/trackEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/trackEvent/index.ts b/packages/destination-actions/src/destinations/insider/trackEvent/index.ts index e494db448e..f6e158f404 100644 --- a/packages/destination-actions/src/destinations/insider/trackEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/trackEvent/index.ts @@ -11,7 +11,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' const action: ActionDefinition = { @@ -21,6 +22,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, event_name: { ...event_name }, diff --git a/packages/destination-actions/src/destinations/insider/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap index 305458a49a..cf865f2bc8 100644 --- a/packages/destination-actions/src/destinations/insider/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/updateUserProfile/__tests__/__snapshots__/snapshot.test.ts.snap @@ -32,6 +32,7 @@ Object { "phone_number": "I[VT4ujd%6E", "uuid": "I[VT4ujd%6E", }, + "not_append": false, }, ], } @@ -49,6 +50,7 @@ Object { }, "uuid": "I[VT4ujd%6E", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/updateUserProfile/generated-types.ts b/packages/destination-actions/src/destinations/insider/updateUserProfile/generated-types.ts index d1fe96e11a..20fdc0aac7 100644 --- a/packages/destination-actions/src/destinations/insider/updateUserProfile/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/updateUserProfile/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * Age of a user. */ diff --git a/packages/destination-actions/src/destinations/insider/updateUserProfile/index.ts b/packages/destination-actions/src/destinations/insider/updateUserProfile/index.ts index 1995c74a58..c456756b7d 100644 --- a/packages/destination-actions/src/destinations/insider/updateUserProfile/index.ts +++ b/packages/destination-actions/src/destinations/insider/updateUserProfile/index.ts @@ -2,6 +2,7 @@ import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' import { userProfilePayload, API_BASE, UPSERT_ENDPOINT, bulkUserProfilePayload } from '../insider-helpers' +import { append_arrays } from '../insider-properties' const action: ActionDefinition = { title: 'Create or Update a User Profile', @@ -20,6 +21,7 @@ const action: ActionDefinition = { description: 'If true, Phone Number will be sent as identifier to Insider', default: true }, + append_arrays: { ...append_arrays }, age: { label: 'Age', type: 'number', diff --git a/packages/destination-actions/src/destinations/insider/userRegisteredEvent/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/destination-actions/src/destinations/insider/userRegisteredEvent/__tests__/__snapshots__/snapshot.test.ts.snap index 66f2ce1822..a5c10e318f 100644 --- a/packages/destination-actions/src/destinations/insider/userRegisteredEvent/__tests__/__snapshots__/snapshot.test.ts.snap +++ b/packages/destination-actions/src/destinations/insider/userRegisteredEvent/__tests__/__snapshots__/snapshot.test.ts.snap @@ -41,6 +41,7 @@ Object { }, "uuid": "9yI*!GTx(Wx$F9", }, + "not_append": true, }, ], } @@ -69,6 +70,7 @@ Object { }, "uuid": "9yI*!GTx(Wx$F9", }, + "not_append": true, }, ], } diff --git a/packages/destination-actions/src/destinations/insider/userRegisteredEvent/generated-types.ts b/packages/destination-actions/src/destinations/insider/userRegisteredEvent/generated-types.ts index 85e5642d23..33ac12e9db 100644 --- a/packages/destination-actions/src/destinations/insider/userRegisteredEvent/generated-types.ts +++ b/packages/destination-actions/src/destinations/insider/userRegisteredEvent/generated-types.ts @@ -9,6 +9,10 @@ export interface Payload { * If true, Phone Number will be sent as identifier to Insider */ phone_number_as_identifier?: boolean + /** + * If enabled, new data for array fields will be appended to the existing values in Insider. + */ + append_arrays?: boolean /** * User's unique identifier. The UUID string is used as identifier when sending data to Insider. UUID is required if the Anonymous Id field is empty. */ diff --git a/packages/destination-actions/src/destinations/insider/userRegisteredEvent/index.ts b/packages/destination-actions/src/destinations/insider/userRegisteredEvent/index.ts index 81a3bd1183..f0346ff4ca 100644 --- a/packages/destination-actions/src/destinations/insider/userRegisteredEvent/index.ts +++ b/packages/destination-actions/src/destinations/insider/userRegisteredEvent/index.ts @@ -7,7 +7,8 @@ import { segment_anonymous_id, timestamp, user_attributes, - uuid + uuid, + append_arrays } from '../insider-properties' import { API_BASE, sendBulkTrackEvents, sendTrackEvent, UPSERT_ENDPOINT } from '../insider-helpers' @@ -18,6 +19,7 @@ const action: ActionDefinition = { fields: { email_as_identifier: { ...email_as_identifier }, phone_number_as_identifier: { ...phone_number_as_identifier }, + append_arrays: { ...append_arrays }, uuid: { ...uuid }, segment_anonymous_id: { ...segment_anonymous_id }, timestamp: { ...timestamp }, From 1daca2106c5b1d9959ec8be719cc0be99c40dc15 Mon Sep 17 00:00:00 2001 From: Seerat Awan Date: Tue, 5 Dec 2023 17:59:34 +0500 Subject: [PATCH 34/34] [Usermaven] Fixed Timezone Offset Calculation (#1747) * feat: usermaven integration init * feat: user identify * feat: usermaven track event * feat: usermaven track event * feat: usermaven added test cases * feat: remove onDelete callback * feat: group action added * feat: test cases * PR review changes partially implemented * feat: refactoring some fields, test cases * feat: PR improvements * fix: minor changes * fix: minor changes * feat: page action and fixed user_anonymous_id typo * fix: added description for the page action * Added missing page action * Added missing user_created_at payload * Added company_id check for company payload * feat: updated events req url to s2s * fix: test cases * chore: remove event_id from the payload * fix: timezone offset calculation (usermaven) --------- Co-authored-by: Azhar --- .../src/destinations/usermaven/request-params.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/destination-actions/src/destinations/usermaven/request-params.ts b/packages/destination-actions/src/destinations/usermaven/request-params.ts index 77e0af3bd2..0cbb358e74 100644 --- a/packages/destination-actions/src/destinations/usermaven/request-params.ts +++ b/packages/destination-actions/src/destinations/usermaven/request-params.ts @@ -86,11 +86,19 @@ export const resolveRequestPayload = (settings: Settings, payload: Record