Skip to content

Commit

Permalink
New Cloud Destination for Kevel (#1784)
Browse files Browse the repository at this point in the history
* initial integration

* t message for your changes. Lines starting

* adding stuf for testAuthentication

* adding unit tests

* tidy up

* removing default tests

* updating mappings

* fixing defaultMapping

* fixing test

* fixing tests
  • Loading branch information
joe-ayoub-segment authored Jan 10, 2024
1 parent 69c5fa5 commit 247f8e0
Show file tree
Hide file tree
Showing 8 changed files with 374 additions and 0 deletions.

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

47 changes: 47 additions & 0 deletions packages/destination-actions/src/destinations/kevel/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type { DestinationDefinition } from '@segment/actions-core'
import type { Settings } from './generated-types'

import syncAudience from './syncAudience'

import syncTraits from './syncTraits'

const destination: DestinationDefinition<Settings> = {
name: 'Kevel',
slug: 'actions-kevel',
description:
'Send Segment user profiles and Segment Audiences to Kevel. Only users with a Segment userId will be synced.',
mode: 'cloud',

authentication: {
scheme: 'custom',
fields: {
networkId: {
label: 'Kevel Network ID',
description: 'Your Kevel Network ID',
type: 'string',
required: true
},
apiKey: {
label: 'Kevel API Key',
description: 'Your Kevel API Key',
type: 'string',
required: true
}
}
},
extendRequest({ settings }) {
return {
headers: {
'X-Adzerk-ApiKey': settings.apiKey,
'Content-Type': 'application/json',
'X-Adzerk-Sdk-Version': 'adzerk-segment-integration:v1.0'
}
}
},
actions: {
syncAudience,
syncTraits
}
}

export default destination
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'

const testDestination = createTestIntegration(Destination)

const goodTrackEvent = createTestEvent({
type: 'track',
userId: 'uid1',
context: {
personas: {
computation_class: 'audience',
computation_key: 'kevel_segment_test_name'
},
traits: {
email: '[email protected]'
}
},
properties: {
audience_key: 'kevel_segment_test_name',
kevel_segment_test_name: true
}
})

const goodIdentifyEvent = createTestEvent({
type: 'identify',
userId: 'uid1',
context: {
personas: {
computation_class: 'audience',
computation_key: 'kevel_segment_test_name'
}
},
traits: {
audience_key: 'kevel_segment_test_name',
kevel_segment_test_name: true
},
properties: undefined
})

const badEvent = createTestEvent({
userId: 'uid1',
context: {
personas: {
computation_key: 'kevel_segment_test_name'
},
traits: {
email: '[email protected]'
}
},
properties: {
audience_key: 'kevel_segment_test_name',
kevel_segment_test_name: true
}
})

describe('Kevel.syncAudience', () => {
it('should not throw an error if the audience creation succeed - track', async () => {
const userId = 'uid1'
const networkId1 = 'networkId1'
const baseUrl = `https://e-${networkId1}.adzerk.net/udb/${networkId1}`

nock(baseUrl)
.post(`/interests?userKey=${userId}`, JSON.stringify(['kevel_segment_test_name']))
.reply(200)

await expect(
testDestination.testAction('syncAudience', {
event: goodTrackEvent,
settings: {
networkId: networkId1,
apiKey: 'apiKey1'
},
useDefaultMappings: true
})
).resolves.not.toThrowError()
})

it('should not throw an error if the audience creation succeed - track', async () => {
const userId = 'uid1'
const networkId1 = 'networkId1'
const baseUrl = `https://e-${networkId1}.adzerk.net/udb/${networkId1}`

nock(baseUrl)
.post(`/interests?userKey=${userId}`, JSON.stringify(['kevel_segment_test_name']))
.reply(200)

await expect(
testDestination.testAction('syncAudience', {
event: goodIdentifyEvent,
settings: {
networkId: networkId1,
apiKey: 'apiKey1'
},
useDefaultMappings: true
})
).resolves.not.toThrowError()
})

it('should throw an error if audience creation event missing mandatory field', async () => {
await expect(
testDestination.testAction('syncAudience', {
event: badEvent,
useDefaultMappings: true
})
).rejects.toThrowError("The root value is missing the required field 'segment_computation_action'")
})
})

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,71 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'

const action: ActionDefinition<Settings, Payload> = {
title: 'Sync Audience',
description: 'Sync a Segment Engage Audience to a Kevel Segment. Only users with a Segment userId will be synced.',
defaultSubscription: 'type = "track" or type = "identify"',
fields: {
segment_computation_key: {
label: 'Audience Key',
description: 'Segment Audience name to which user identifier should be added or removed',
type: 'string',
unsafe_hidden: true,
required: true,
default: {
'@path': '$.context.personas.computation_key'
}
},
segment_computation_action: {
label: 'Segment Computation Action',
description:
"Segment computation class used to determine if input event is from an Engage Audience'. Value must be = 'audience'.",
type: 'string',
unsafe_hidden: true,
required: true,
default: {
'@path': '$.context.personas.computation_class'
},
choices: [{ label: 'audience', value: 'audience' }]
},
segment_user_id: {
label: 'User ID',
description: "The user's unique ID",
type: 'string',
unsafe_hidden: true,
required: true,
default: { '@path': '$.userId' }
},
traits_or_props: {
label: 'Traits or properties object',
description: 'A computed object for track and identify events. This field should not need to be edited.',
type: 'object',
required: true,
unsafe_hidden: true,
default: {
'@if': {
exists: { '@path': '$.properties' },
then: { '@path': '$.properties' },
else: { '@path': '$.traits' }
}
}
}
},
perform: async (request, data) => {
const settings = data.settings

const baseUrl = `https://e-${settings.networkId}.adzerk.net/udb/${settings.networkId}`

const payload = data.payload

const audienceValue = payload.traits_or_props[payload.segment_computation_key]

return request(`${baseUrl}/interests?userKey=${payload.segment_user_id}`, {
json: [payload.segment_computation_key],
method: audienceValue ? 'POST' : 'DELETE'
})
}
}

export default action
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'

const testDestination = createTestIntegration(Destination)

const goodIdentifyEvent = createTestEvent({
type: 'identify',
userId: 'uid1',

traits: {
first_name: 'Billy',
last_name: 'Bob'
}
})

describe('Kevel.syncTraits', () => {
it('should fetch and merge traits, and then not throw an error - track', async () => {
const userId = 'uid1'
const networkId1 = 'networkId1'
const baseUrl = `https://e-${networkId1}.adzerk.net/udb/${networkId1}`

const allTraits = {
age: 24,
first_name: 'Billy',
last_name: 'Bob'
}

nock(baseUrl)
.get(`/read?userKey=${userId}`)
.reply(
200,
JSON.stringify({
custom: {
age: 24
}
})
)

nock(baseUrl).post(`/customProperties?userKey=${userId}`, JSON.stringify(allTraits)).reply(200)

await expect(
testDestination.testAction('syncTraits', {
event: goodIdentifyEvent,
settings: {
networkId: networkId1,
apiKey: 'apiKey1'
},
useDefaultMappings: true
})
).resolves.not.toThrowError()
})
})

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,47 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'

const action: ActionDefinition<Settings, Payload> = {
title: 'Sync Traits',
description: 'Sync user profile traits from Segment to Kevel',
defaultSubscription: 'type = "identify"',
fields: {
segment_user_id: {
label: 'User ID',
description: "The user's unique ID",
type: 'string',
required: true,
default: { '@path': '$.userId' }
},
traits: {
label: 'Traits',
description: "The user's profile traits / attributes",
type: 'object',
required: true,
default: { '@path': '$.traits' }
}
},
perform: async (request, data) => {
const settings = data.settings

const baseUrl = `https://e-${settings.networkId}.adzerk.net/udb/${settings.networkId}`

const payload = data.payload

const existingResponse = await request(`${baseUrl}/read?userKey=${payload.segment_user_id}`, {
method: 'GET'
})

const existingRecord = await existingResponse.json()

const mergedTraits = { ...existingRecord?.custom, ...payload.traits }

return request(`${baseUrl}/customProperties?userKey=${payload.segment_user_id}`, {
json: mergedTraits,
method: 'POST'
})
}
}

export default action

0 comments on commit 247f8e0

Please sign in to comment.