Skip to content

Added Functionaility for Custom Attributes and Subscribers API from Attentive #2873

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

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

hi @duynguyen100 - can you undo this change please? This affects the entire solution and it's a file that should be changed.

"ws": "^8.5.0"
},
"resolutions": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -15,7 +15,7 @@
},
"typings": "./dist/esm",
"dependencies": {
"@segment/analytics-browser-actions-braze": "^1.85.0",
"@segment/analytics-browser-actions-braze": "^1.86.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

hi @duynguyen100 . Did you mean to change this file? If not can you revert the change to this file please?

"@segment/browser-destination-runtime": "^1.77.0"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@segment/analytics-browser-actions-braze",
"version": "1.85.0",
"version": "1.86.0",
Copy link
Contributor

Choose a reason for hiding this comment

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

hi @duynguyen100 . Did you mean to change this file? If not can you revert the change to this file please?

"license": "MIT",
"publishConfig": {
"access": "public",
Expand Down
4 changes: 2 additions & 2 deletions packages/destination-actions/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

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

hi @duynguyen100 . Did you mean to change this file? If not can you revert the change to this file please?

"nock": "^13.1.4"
},
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -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: '[email protected]'
}
},
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<SegmentEvent>

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: '[email protected]',
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 === '[email protected]' &&
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.'))
})
})

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

Original file line number Diff line number Diff line change
@@ -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<Settings, Payload> = {
title: 'Custom Attributes',
description: 'Send custom attributes to Attentive.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
description: 'Send custom attributes to Attentive.',
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// types.ts

export interface CustomAttributes {
user: User // User object
attributes: Record<string, any> // 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
}
}
Loading