From 3f0a8c81ad7a2cf2775fe16b4c9e1ea172e1aa1d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 11 Mar 2025 16:03:24 -0700 Subject: [PATCH] Publish (#2790) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- package.json | 2 +- .../braze-cloud-plugins/package.json | 4 +- .../destinations/braze/package.json | 2 +- packages/destination-actions/package.json | 4 +- .../customattributes/__tests__/index.test.ts | 120 +++++++++++++++ .../customattributes/generated-types.ts | 36 +++++ .../attentive/customattributes/index.ts | 128 ++++++++++++++++ .../attentive/customattributes/types.ts | 16 ++ .../src/destinations/attentive/index.ts | 28 +++- .../subscribers/__tests__/index.test.ts | 144 ++++++++++++++++++ .../attentive/subscribers/generated-types.ts | 29 ++++ .../attentive/subscribers/index.ts | 88 +++++++++++ .../attentive/subscribers/type.ts | 25 +++ packages/destinations-manifest/package.json | 6 +- 14 files changed, 619 insertions(+), 13 deletions(-) create mode 100644 packages/destination-actions/src/destinations/attentive/customattributes/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/attentive/customattributes/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/attentive/customattributes/index.ts create mode 100644 packages/destination-actions/src/destinations/attentive/customattributes/types.ts create mode 100644 packages/destination-actions/src/destinations/attentive/subscribers/__tests__/index.test.ts create mode 100644 packages/destination-actions/src/destinations/attentive/subscribers/generated-types.ts create mode 100644 packages/destination-actions/src/destinations/attentive/subscribers/index.ts create mode 100644 packages/destination-actions/src/destinations/attentive/subscribers/type.ts diff --git a/package.json b/package.json index b8548dadc7..d554ce843d 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "timers-browserify": "^2.0.12", "ts-jest": "^27.0.7", "ts-node": "^9.1.1", - "typescript": "4.9.5", + "typescript": "^5.8.3", "ws": "^8.5.0" }, "resolutions": { diff --git a/packages/browser-destinations/destinations/braze-cloud-plugins/package.json b/packages/browser-destinations/destinations/braze-cloud-plugins/package.json index e545daff62..ad2b39fea8 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.85.0", + "version": "1.86.0", "license": "MIT", "publishConfig": { "access": "public", @@ -15,7 +15,7 @@ }, "typings": "./dist/esm", "dependencies": { - "@segment/analytics-browser-actions-braze": "^1.85.0", + "@segment/analytics-browser-actions-braze": "^1.86.0", "@segment/browser-destination-runtime": "^1.77.0" }, "peerDependencies": { diff --git a/packages/browser-destinations/destinations/braze/package.json b/packages/browser-destinations/destinations/braze/package.json index 1789b3f50a..48ff5617e6 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.85.0", + "version": "1.86.0", "license": "MIT", "publishConfig": { "access": "public", diff --git a/packages/destination-actions/package.json b/packages/destination-actions/package.json index 2715bea25a..b1fb21b02f 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.372.0", + "version": "3.373.0", "repository": { "type": "git", "url": "https://github.com/segmentio/action-destinations", @@ -36,7 +36,7 @@ "@types/google-libphonenumber": "^7.4.23", "@types/jest": "^27.0.0", "@types/ssh2-sftp-client": "^9.0.0", - "jest": "^27.3.1", + "jest": "^27.5.1", "nock": "^13.1.4" }, "dependencies": { diff --git a/packages/destination-actions/src/destinations/attentive/customattributes/__tests__/index.test.ts b/packages/destination-actions/src/destinations/attentive/customattributes/__tests__/index.test.ts new file mode 100644 index 0000000000..d3e2506eec --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customattributes/__tests__/index.test.ts @@ -0,0 +1,120 @@ +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: 'Custom Attribute Event', + messageId: '123e4567-e89b-12d3-a456-426614174000', + type: 'track', + userId: '123e4567-e89b-12d3-a456-426614174000', + context: { + traits: { + phone: '+3538675765689', + email: 'test@test.com' + } + }, + properties: { + // Properties section + age: '24', + birthday: '1986-11-16', + 'sign up': '2021-04-23T16:04:33Z', + 'favorite team': 'Minnesota Vikings', + 'Gift card balance': '50.89', + VIP: 'TRUE' + } +} as Partial + +const mapping = { + userIdentifiers: { + phone: { '@path': '$.context.traits.phone' }, + email: { '@path': '$.context.traits.email' }, + clientUserId: { '@path': '$.userId' } + }, + properties: { '@path': '$.properties' } +} + +const _expectedPayload = { + properties: { + // Expected payload for the API (properties are mapped directly) + age: '24', + birthday: '1986-11-16', + 'sign up': '2021-04-23T16:04:33Z', + 'favorite team': 'Minnesota Vikings', + 'Gift card balance': '50.89', + VIP: 'TRUE' + }, + user: { + phone: '+3538675765689', + email: 'test@test.com', + externalIdentifiers: { + clientUserId: '123e4567-e89b-12d3-a456-426614174000' + } + } +} + +beforeEach((done) => { + testDestination = createTestIntegration(Definition) + nock.cleanAll() + done() +}) + +describe('Attentive.customAttributes', () => { + it('should send custom attributes to Attentive', async () => { + const event = createTestEvent(validPayload) + + // Mock the correct API endpoint and response for custom attributes + nock('https://api.attentivemobile.com') + .post('/v1/attributes/custom', (body) => { + return ( + body.properties.age === '24' && + body.properties.birthday === '1986-11-16' && + body.user.phone === '+3538675765689' && + body.user.email === 'test@test.com' && + body.user.externalIdentifiers.clientUserId === '123e4567-e89b-12d3-a456-426614174000' + ) + }) + .matchHeader('authorization', 'Bearer test-api-key') + .matchHeader('content-type', 'application/json') + .reply(200, {}) + + // Test sending the custom attributes + const responses = await testDestination.testAction('customAttributes', { + 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('customAttributes', { + 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/customattributes/generated-types.ts b/packages/destination-actions/src/destinations/attentive/customattributes/generated-types.ts new file mode 100644 index 0000000000..3e08cb7ff7 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customattributes/generated-types.ts @@ -0,0 +1,36 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * 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 + } + /** + * Custom attributes to associate with the user. + */ + 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/customattributes/index.ts b/packages/destination-actions/src/destinations/attentive/customattributes/index.ts new file mode 100644 index 0000000000..959037ece9 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customattributes/index.ts @@ -0,0 +1,128 @@ +import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { CustomAttribute, User } from './types' + +const action: ActionDefinition = { + title: 'Custom Attributes', + description: 'Send custom attributes to Attentive.', + defaultSubscription: 'type = "identify"', + fields: { + 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: 'Custom attributes to associate with the user.', + 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, + properties, + occurredAt, + userIdentifiers: { phone, email, clientUserId, ...customIdentifiers } + } = payload + + // Ensure at least one identifier exists + if (!email && !phone && !clientUserId && Object.keys(customIdentifiers).length === 0) { + throw new PayloadValidationError('At least one user identifier is required.') + } + + // Construct the custom attributes payload + const json: CustomAttribute = { + properties, + externalEventId, + occurredAt, + user: { + phone, + email, + ...(clientUserId || customIdentifiers + ? { + externalIdentifiers: { + ...(clientUserId ? { clientUserId } : undefined), + ...(Object.entries(customIdentifiers).length > 0 ? { customIdentifiers } : undefined) + } + } + : {}) + } as User + } + + // Send the request to the Attentive API + return request('https://api.attentivemobile.com/v1/attributes/custom', { + method: 'post', + json + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/attentive/customattributes/types.ts b/packages/destination-actions/src/destinations/attentive/customattributes/types.ts new file mode 100644 index 0000000000..44b5a42aac --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/customattributes/types.ts @@ -0,0 +1,16 @@ +// types.ts + +export interface CustomAttributes { + user: User // User object + attributes: Record // Custom attributes to be sent to Attentive +} + +export interface User { + phone?: string // Optional phone number + email?: string // Optional email address + externalIdentifiers?: { + // Optional external identifiers + clientUserId?: string // Optional custom user ID + [key: string]: string | undefined // Additional custom identifiers + } +} diff --git a/packages/destination-actions/src/destinations/attentive/index.ts b/packages/destination-actions/src/destinations/attentive/index.ts index 12b4bbe59b..2a161780d3 100644 --- a/packages/destination-actions/src/destinations/attentive/index.ts +++ b/packages/destination-actions/src/destinations/attentive/index.ts @@ -1,12 +1,14 @@ import { DestinationDefinition, defaultValues } from '@segment/actions-core' import type { Settings } from './generated-types' -import customEvents from './customEvents' +import customEvents from './customEvents' // Existing action for custom events +import customAttributes from './customAttributes' // Existing action for custom attributes +import subscribers from './subscribers' // New action for managing subscribers const destination: DestinationDefinition = { name: 'Attentive', - slug: 'actions-attentive', + slug: 'actions-attentive', // Keep default slug mode: 'cloud', - description: 'Send Segment analytics events to Attentive.', + description: 'Send Segment analytics events, custom attributes, and manage subscriber subscriptions in Attentive.', authentication: { scheme: 'custom', fields: { @@ -35,9 +37,13 @@ const destination: DestinationDefinition = { } } }, + actions: { - customEvents + customEvents, // Send analytics events to Attentive + customAttributes, // Send custom attributes to Attentive + subscribers // Manage subscriber subscriptions }, + presets: [ { name: 'Track Event', @@ -45,6 +51,20 @@ const destination: DestinationDefinition = { partnerAction: 'customEvents', mapping: defaultValues(customEvents.fields), type: 'automatic' + }, + { + name: 'Track Custom Attributes', + subscribe: 'type = "identify"', // Trigger custom attributes on identify events + partnerAction: 'customAttributes', + mapping: defaultValues(customAttributes.fields), + type: 'automatic' + }, + { + name: 'Subscribe User', + subscribe: 'type = "identify" and traits.phone != null', // Trigger on identify with `subscribed: true` + partnerAction: 'subscribers', + mapping: defaultValues(subscribers.fields), + type: 'automatic' } ] } diff --git a/packages/destination-actions/src/destinations/attentive/subscribers/__tests__/index.test.ts b/packages/destination-actions/src/destinations/attentive/subscribers/__tests__/index.test.ts new file mode 100644 index 0000000000..76021d39d9 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/subscribers/__tests__/index.test.ts @@ -0,0 +1,144 @@ +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: 'Identify Event', + messageId: '123e4567-e89b-12d3-a456-426614174000', + type: 'track', + userId: '123e4567-e89b-12d3-a456-426614174000', + context: { + traits: { + phone: '+3538675765689', + email: 'test@test.com' + } + }, + properties: {} // No properties for subscription +} as Partial + +const mapping = { + userIdentifiers: { + phone: { '@path': '$.context.traits.phone' }, + email: { '@path': '$.context.traits.email' } + }, + subscriptionType: 'MARKETING', + locale: { language: 'en', country: 'US' } +} + +const expectedPayload = { + user: { + phone: '+3538675765689', + email: 'test@test.com' + }, + subscriptionType: 'MARKETING', + locale: { + language: 'en', + country: 'US' + } +} + +beforeEach((done) => { + testDestination = createTestIntegration(Definition) + nock.cleanAll() + done() +}) + +describe('Attentive.subscribers', () => { + it('should send a subscription request to Attentive', async () => { + const event = createTestEvent(validPayload) + + // Mock the correct API endpoint and response for subscriptions + nock('https://api.attentivemobile.com', { + reqheaders: { + authorization: 'Bearer test-api-key', + 'content-type': 'application/json', + 'user-agent': 'Segment (Actions)' + } + }) + .post('/v1/subscriptions', expectedPayload) + .reply(200, {}) + + // Test sending the subscription request + const responses = await testDestination.testAction('subscribers', { + event, + settings, + useDefaultMappings: true, + mapping + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + }) + + it('should throw error if no user identifiers provided', async () => { + const badPayload = { + ...validPayload + } + delete badPayload?.context?.traits?.phone + delete badPayload?.context?.traits?.email + + const event = createTestEvent(badPayload) + + await expect( + testDestination.testAction('subscribers', { + event, + settings, + useDefaultMappings: true, + mapping + }) + ).rejects.toThrowError(new PayloadValidationError('At least one user identifier (phone or email) is required.')) + }) + + it('should not throw error if only one identifier is provided', async () => { + const partialPayload = { + ...validPayload, + context: { + traits: { + phone: '+3538675765689' + } + } + } + + const event = createTestEvent(partialPayload) + + // Mock the correct API endpoint and response for subscriptions with only phone + nock('https://api.attentivemobile.com', { + reqheaders: { + authorization: 'Bearer test-api-key', + 'content-type': 'application/json', + 'user-agent': 'Segment (Actions)' + } + }) + .post('/v1/subscriptions', { + user: { + phone: '+3538675765689' + }, + subscriptionType: 'MARKETING', + locale: { + language: 'en', + country: 'US' + } + }) + .reply(200, {}) + + // Test sending the subscription request with only phone + const responses = await testDestination.testAction('subscribers', { + event, + settings, + useDefaultMappings: true, + mapping + }) + + expect(responses.length).toBe(1) + expect(responses[0].status).toBe(200) + }) +}) diff --git a/packages/destination-actions/src/destinations/attentive/subscribers/generated-types.ts b/packages/destination-actions/src/destinations/attentive/subscribers/generated-types.ts new file mode 100644 index 0000000000..327ad8a591 --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/subscribers/generated-types.ts @@ -0,0 +1,29 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * At least one identifier (phone or email) is required. + */ + userIdentifiers: { + /** + * The user's phone number in E.164 format. + */ + phone?: string + /** + * The user's email address. + */ + email?: string + [k: string]: unknown + } + /** + * Type of subscription (MARKETING or TRANSACTIONAL) + */ + subscriptionType: string + /** + * User locale (language and country) + */ + locale?: { + language: string + country: string + } +} diff --git a/packages/destination-actions/src/destinations/attentive/subscribers/index.ts b/packages/destination-actions/src/destinations/attentive/subscribers/index.ts new file mode 100644 index 0000000000..1e5bf3d2af --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/subscribers/index.ts @@ -0,0 +1,88 @@ +import { ActionDefinition, PayloadValidationError } from '@segment/actions-core' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { SubscriberRequest } from './types' + +const action: ActionDefinition = { + title: 'Subscribe User to Attentive', + description: 'Send a subscription request to Attentive.', + defaultSubscription: 'type = "identify"', + fields: { + userIdentifiers: { + label: 'User Identifiers', + description: 'At least one identifier (phone or email) is required.', + type: 'object', + required: true, + additionalProperties: true, + 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 + } + } + }, + subscriptionType: { + label: 'Subscription Type', + description: 'Type of subscription (MARKETING or TRANSACTIONAL)', + type: 'string', + required: true, + default: 'MARKETING' + }, + locale: { + label: 'Locale', + description: 'User locale (language and country)', + type: 'object', + required: false, + properties: { + language: { + label: 'Language', + type: 'string', + required: true + }, + country: { + label: 'Country', + type: 'string', + required: true + } + } + } + }, + perform: (request, { payload, settings }) => { + const { phone, email } = payload.userIdentifiers + const { subscriptionType, locale } = payload + const localeData = locale || { language: 'en', country: 'US' } + + // Validate that at least one user identifier (phone or email) is provided + if (!phone && !email) { + throw new PayloadValidationError('At least one user identifier (phone or email) is required.') + } + + const requestBody: SubscriberRequest = { + user: { + phone, + email + }, + subscriptionType, + locale: localeData + } + + return request('https://api.attentivemobile.com/v1/subscriptions', { + method: 'post', + json: requestBody, + headers: { + Authorization: `Bearer ${settings.apiKey}` + } + }) + } +} + +export default action diff --git a/packages/destination-actions/src/destinations/attentive/subscribers/type.ts b/packages/destination-actions/src/destinations/attentive/subscribers/type.ts new file mode 100644 index 0000000000..57a105cd0f --- /dev/null +++ b/packages/destination-actions/src/destinations/attentive/subscribers/type.ts @@ -0,0 +1,25 @@ +export interface SubscriberRequest { + user: SubscriberUser // User object (required) + signUpSourceId?: string // Optional: Unique sign-up source ID + locale?: Locale // Optional: Locale object + subscriptionType: 'MARKETING' | 'TRANSACTIONAL' // Required: Subscription type + singleOptIn?: boolean // Optional: Skip legal/reply confirmation message +} + +export interface SubscriberUser { + phone?: string // Optional: Phone number in E.164 format + email?: string // Optional: Email address +} + +export interface Locale { + language: string // Required if locale is used (e.g., "en") + country: string // Required if locale is used (e.g., "US") +} + +export interface Payload { + userIdentifiers: SubscriberUser // userIdentifiers instead of context + signUpSourceId?: string + subscriptionType: 'MARKETING' | 'TRANSACTIONAL' + locale?: Locale // Corrected to be an object instead of a string + singleOptIn?: boolean +} diff --git a/packages/destinations-manifest/package.json b/packages/destinations-manifest/package.json index 19ba6066bd..ed14fe44c5 100644 --- a/packages/destinations-manifest/package.json +++ b/packages/destinations-manifest/package.json @@ -1,6 +1,6 @@ { "name": "@segment/destinations-manifest", - "version": "1.107.0", + "version": "1.108.0", "publishConfig": { "access": "public", "registry": "https://registry.npmjs.org" @@ -16,8 +16,8 @@ "@segment/analytics-browser-actions-adobe-target": "^1.79.0", "@segment/analytics-browser-actions-algolia-plugins": "^1.55.0", "@segment/analytics-browser-actions-amplitude-plugins": "^1.78.0", - "@segment/analytics-browser-actions-braze": "^1.85.0", - "@segment/analytics-browser-actions-braze-cloud-plugins": "^1.85.0", + "@segment/analytics-browser-actions-braze": "^1.86.0", + "@segment/analytics-browser-actions-braze-cloud-plugins": "^1.86.0", "@segment/analytics-browser-actions-bucket": "^1.59.0", "@segment/analytics-browser-actions-cdpresolution": "^1.65.0", "@segment/analytics-browser-actions-commandbar": "^1.78.0",