From b1c9e56defa9dbcd8064c44d99e95ec66af2fcc2 Mon Sep 17 00:00:00 2001 From: Turner Nelson <38872134+wtnelso@users.noreply.github.com> Date: Tue, 26 Nov 2024 06:59:27 -0600 Subject: [PATCH] First push with CustomEvents Destination Action (#2570) * Saving progress * Update packages/destination-actions/src/destinations/attentive/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update packages/destination-actions/src/destinations/attentive/customEvents/index.ts Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> * Update index.ts Added default mapping for phone field * refacting the fields and request * correcting yarn version * minor tweaks * minor tweaks * adding types * Update index.test.ts * Update index.test.ts * Update index.test.ts * Update index.test.ts * adding types * New endpoints * added of endpoints * bug fixes * config file added * index updates * updating snapshot and fixing a field type issue * adding a unit test * new unit test * fixing breaking test * removing snapshot --------- Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com> Co-authored-by: Joe Ayoub --- .../customEvents/__tests__/index.test.ts | 102 +++++++++++++ .../attentive/customEvents/generated-types.ts | 40 ++++++ .../attentive/customEvents/index.ts | 135 ++++++++++++++++++ .../attentive/customEvents/types.ts | 16 +++ .../destinations/attentive/generated-types.ts | 8 ++ .../src/destinations/attentive/index.ts | 52 +++++++ 6 files changed, 353 insertions(+) create mode 100644 packages/destination-actions/src/destinations/attentive/customEvents/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/attentive/customEvents/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/attentive/customEvents/index.ts create mode 100644 packages/destination-actions/src/destinations/attentive/customEvents/types.ts create mode 100644 packages/destination-actions/src/destinations/attentive/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/attentive/index.ts diff --git a/packages/destination-actions/src/destinations/attentive/customEvents/__tests__/index.test.ts b/packages/destination-actions/src/destinations/attentive/customEvents/__tests__/index.test.ts new file mode 100644 index 0000000000..421654cd30 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customEvents/__tests__/index.test.ts @@ -0,0 +1,102 @@ +import nock from 'nock' +import { createTestEvent, createTestIntegration, SegmentEvent, PayloadValidationError } from '@segment/actions-core' +import Definition from '../../index' +import { Settings } from '../../generated-types' + +let testDestination = createTestIntegration(Definition) +const timestamp = '2024-01-08T13:52:50.212Z' + +const settings: Settings = { + apiKey: 'test-api-key' +} + +const validPayload = { + timestamp: timestamp, + event: 'Event Type 1', + messageId: '123e4567-e89b-12d3-a456-426614174000', + type: 'track', + userId: '123e4567-e89b-12d3-a456-426614174000', + context: { + traits: { + phone: '+3538675765689', + email: 'test@test.com' + } + }, + properties: { + tracking_url: 'https://tracking-url.com', + product_name: 'Product X' + } +} as Partial + +const mapping = { + type: { '@path': '$.event' }, + userIdentifiers: { + phone: { '@path': '$.context.traits.phone' }, + email: { '@path': '$.context.traits.email' }, + clientUserId: { '@path': '$.userId' } + }, + properties: { '@path': '$.properties' }, + externalEventId: { '@path': '$.messageId' }, + occurredAt: { '@path': '$.timestamp' } +} + +const expectedPayload = { + type: 'Event Type 1', + properties: { + tracking_url: 'https://tracking-url.com', + product_name: 'Product X' + }, + externalEventId: '123e4567-e89b-12d3-a456-426614174000', + occurredAt: '2024-01-08T13:52:50.212Z', + user: { + phone: '+3538675765689', + email: 'test@test.com', + externalIdentifiers: { + clientUserId: '123e4567-e89b-12d3-a456-426614174000' + } + } +} + +beforeEach((done) => { + testDestination = createTestIntegration(Definition) + nock.cleanAll() + done() +}) + +describe('Attentive.customEvents', () => { + it('should send a custom event to Attentive', async () => { + const event = createTestEvent(validPayload) + + nock('https://api.attentivemobile.com').post('/v1/events/custom', expectedPayload).reply(200, {}) + + const responses = await testDestination.testAction('customEvents', { + event, + settings, + useDefaultMappings: true, + mapping + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + }) + + it('should throw error if no identifiers provided', async () => { + const badPayload = { + ...validPayload + } + delete badPayload?.context?.traits?.phone + delete badPayload?.context?.traits?.email + badPayload.userId = undefined + + const event = createTestEvent(badPayload) + + await expect( + testDestination.testAction('customEvents', { + event, + settings, + useDefaultMappings: true, + mapping + }) + ).rejects.toThrowError(new PayloadValidationError('At least one user identifier is required.')) + }) +}) diff --git a/packages/destination-actions/src/destinations/attentive/customEvents/generated-types.ts b/packages/destination-actions/src/destinations/attentive/customEvents/generated-types.ts new file mode 100644 index 0000000000..44df57ea91 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customEvents/generated-types.ts @@ -0,0 +1,40 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * The type of event. This name is case sensitive. "Order shipped" and "Order Shipped" would be considered different event types. + */ + type: string + /** + * At least one identifier is required. Custom identifiers can be added as additional key:value pairs. + */ + userIdentifiers: { + /** + * The user's phone number in E.164 format. + */ + phone?: string + /** + * The user's email address. + */ + email?: string + /** + * A primary ID for a user. Should be a UUID. + */ + clientUserId?: string + [k: string]: unknown + } + /** + * Metadata to associate with the event. + */ + properties?: { + [k: string]: unknown + } + /** + * A unique identifier representing this specific event. Should be a UUID format. + */ + externalEventId?: string + /** + * Timestamp for the event, ISO 8601 format. + */ + occurredAt?: string +} diff --git a/packages/destination-actions/src/destinations/attentive/customEvents/index.ts b/packages/destination-actions/src/destinations/attentive/customEvents/index.ts new file mode 100644 index 0000000000..5213b5de2f --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customEvents/index.ts @@ -0,0 +1,135 @@ +import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { CustomEvent, User } from './types' + +const action: ActionDefinition = { + title: 'Custom Events', + description: 'Send Segment analytics events to Attentive.', + defaultSubscription: 'type = "track"', + fields: { + type: { + label: 'Type', + description: 'The type of event. This name is case sensitive. "Order shipped" and "Order Shipped" would be considered different event types.', + type: 'string', + required: true, + default: { + '@path': '$.event' + } + }, + userIdentifiers: { + label: 'User Identifiers', + description: 'At least one identifier is required. Custom identifiers can be added as additional key:value pairs.', + type: 'object', + required: true, + additionalProperties: true, + defaultObjectUI: 'keyvalue:only', + properties: { + phone: { + label: 'Phone', + description: "The user's phone number in E.164 format.", + type: 'string', + required: false + }, + email: { + label: 'Email', + description: "The user's email address.", + type: 'string', + format: 'email', + required: false + }, + clientUserId: { + label: 'Client User ID', + description: 'A primary ID for a user. Should be a UUID.', + type: 'string', + format: 'uuid', + required: false + } + }, + default: { + phone: { + '@if': { + exists: { '@path': '$.context.traits.phone' }, + then: { '@path': '$.context.traits.phone' }, + else: { '@path': '$.properties.phone' } + } + }, + email: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + clientUserId: { '@path': '$.userId' } + } + }, + properties: { + label: 'Properties', + description: 'Metadata to associate with the event.', + type: 'object', + required: false, + default: { + '@path': '$.properties' + } + }, + externalEventId: { + label: 'External Event Id', + description: 'A unique identifier representing this specific event. Should be a UUID format.', + type: 'string', + format: 'uuid', + required: false, + default: { + '@path': '$.messageId' + } + }, + occurredAt: { + label: 'Occurred At', + description: 'Timestamp for the event, ISO 8601 format.', + type: 'string', + required: false, + default: { + '@path': '$.timestamp' + } + } + }, + perform: (request, { payload }) => { + const { + externalEventId, + type, + properties, + occurredAt, + userIdentifiers: { phone, email, clientUserId, ...customIdentifiers } + } = payload + + if (!email && !phone && !clientUserId && Object.keys(customIdentifiers).length === 0) { + throw new PayloadValidationError('At least one user identifier is required.') + } + + const json: CustomEvent = { + type, + properties, + externalEventId, + occurredAt, + user: { + phone, + email, + ...(clientUserId || customIdentifiers + ? { + externalIdentifiers: { + ...(clientUserId ? { clientUserId } : undefined), + ...(Object.entries(customIdentifiers).length>0 ? { customIdentifiers } : undefined) + } + } + : {}) + } as User + } + + return request('https://api.attentivemobile.com/v1/events/custom', { + method: 'post', + json + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/attentive/customEvents/types.ts b/packages/destination-actions/src/destinations/attentive/customEvents/types.ts new file mode 100644 index 0000000000..9eddef039c --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customEvents/types.ts @@ -0,0 +1,16 @@ +export interface CustomEvent { + type: string + properties?: Record + externalEventId?: string + occurredAt?: string + user: User +} + +export interface User { + phone?: string + email?: string + externalIdentifiers?: { + clientUserId?: string + [key: string]: string | undefined + } +} diff --git a/packages/destination-actions/src/destinations/attentive/generated-types.ts b/packages/destination-actions/src/destinations/attentive/generated-types.ts new file mode 100644 index 0000000000..9ab11e7232 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/generated-types.ts @@ -0,0 +1,8 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Your Attentive API Key. + */ + apiKey: string +} diff --git a/packages/destination-actions/src/destinations/attentive/index.ts b/packages/destination-actions/src/destinations/attentive/index.ts new file mode 100644 index 0000000000..12b4bbe59b --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/index.ts @@ -0,0 +1,52 @@ +import { DestinationDefinition, defaultValues } from '@segment/actions-core' +import type { Settings } from './generated-types' +import customEvents from './customEvents' + +const destination: DestinationDefinition = { + name: 'Attentive', + slug: 'actions-attentive', + mode: 'cloud', + description: 'Send Segment analytics events to Attentive.', + authentication: { + scheme: 'custom', + fields: { + apiKey: { + label: 'API Key', + description: 'Your Attentive API Key.', + type: 'string', + required: true + } + }, + testAuthentication: async (request, { settings }) => { + return request('https://api.attentivemobile.com/v1/me', { + method: 'GET', + headers: { + Authorization: `Bearer ${settings.apiKey}` + } + }) + } + }, + + extendRequest({ settings }) { + return { + headers: { + Authorization: `Bearer ${settings.apiKey}`, + 'Content-Type': 'application/json' + } + } + }, + actions: { + customEvents + }, + presets: [ + { + name: 'Track Event', + subscribe: 'type = "track"', + partnerAction: 'customEvents', + mapping: defaultValues(customEvents.fields), + type: 'automatic' + } + ] +} + +export default destination