Skip to content
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

Adjust Action Destination: First implementation + unit tests. #2144

Merged
merged 14 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
@@ -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-adjust'

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()
}
})
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { RequestClient } from '@segment/actions-core/create-request-client'
import { ModifiedResponse } from '@segment/actions-core/types'

import { AdjustPayload } from './types'
import { Settings } from './generated-types'
import { Payload } from './sendEvent/generated-types'

export function validatePayload(
payload: Payload,
eventData: { [key: string]: unknown },
settings: Settings
): AdjustPayload {
if (!payload.app_token && !settings.default_app_token) {
throw new Error('Either app_token in the mapping, or default_app_token in settings, must be provided.')
}

if (!payload.event_token && !settings.default_event_token) {
throw new Error('Either event_token in the mapping, or default_event_token in settings, must be provided.')
}

const adjustPayload: AdjustPayload = {
app_token: String(payload.app_token || settings.default_app_token),
event_token: String(payload.event_token || settings.default_event_token),
environment: settings.environment,
s2s: 1,
callback_params: JSON.stringify(payload)
}

if (settings.send_event_creation_time) {
if (!eventData.timestamp) {
throw new Error('Event timestamp is required when send_event_creation_time is enabled.')
}

adjustPayload.created_at_unix = parseInt((new Date(String(eventData.timestamp)).getTime() / 1000).toFixed(0))
}

return adjustPayload
}

/**
* This is ready for batching, but batching is not implemented here for now.
* @param request The request client.
* @param events The events.
* @returns An array of responses.
*/
export async function sendEvents(request: RequestClient, events: AdjustPayload[]): Promise<any[]> {
const responses: Array<ModifiedResponse<unknown>> = []
for (const event of events) {
const response = await request('https://s2s.adjust.com/event', {
method: 'POST',
body: JSON.stringify(event)
})

responses.push(response)
}

return responses
}

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

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

import sendEvent from './sendEvent'

const destination: DestinationDefinition<Settings> = {
name: 'Adjust',
slug: 'actions-adjust',
mode: 'cloud',

authentication: {
scheme: 'custom',
fields: {
environment: {
label: 'Environment',
description: 'The environment for your Adjust account.',
type: 'string',
required: true,
choices: [
{ label: 'Production', value: 'production' },
{ label: 'Sandbox', value: 'sandbox' }
],
default: 'production'
},
default_app_token: {
label: 'Default App Token',
description: 'The app token for your Adjust account. Can be overridden in the event mapping.',
type: 'string',
required: false
},
default_event_token: {
label: 'Default Event Token',
description: 'The default event token. Can be overridden in the event mapping.',
type: 'string',
required: false
},
send_event_creation_time: {
label: 'Send Event Creation Time',
description: 'Send the event creation time to Adjust.',
type: 'boolean',
required: false,
default: false
}
}
},

actions: {
sendEvent
}
}

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 { createTestEvent, createTestIntegration } from '@segment/actions-core'

import Destination from '../../index'

const testDestination = createTestIntegration(Destination)

const DEVICE_ID = 'device-id' // References device.id
const ADVERTISING_ID = 'foobar' // References device.advertisingId
const DEVICE_TYPE = 'ios' // References device.type

describe('Adjust.sendEvent', () => {
describe('Success cases', () => {
it('should send an event to Adjust, default mappings, default parameters in Settings', async () => {
nock('https://s2s.adjust.com').post('/event').reply(200, { status: 'OK' })

const goodEvent = createTestEvent({
type: 'track',
context: {
device: {
id: DEVICE_ID,
advertisingId: ADVERTISING_ID,
type: DEVICE_TYPE
},
library: {
name: 'analytics-ios',
version: '4.0.0'
}
},
properties: {
revenue: 10,
currency: 'USD'
},
timestamp: '2024-07-05T12:34:56.789Z'
})

const responses = await testDestination.testAction('sendEvent', {
event: goodEvent,
useDefaultMappings: true,
settings: {
environment: 'sandbox',
default_app_token: 'app-token',
default_event_token: 'event-token',
send_event_creation_time: true
}
})

expect(responses).toHaveLength(1)
expect(responses[0].content).toBeDefined()
expect(responses[0].content).toEqual(JSON.stringify({ status: 'OK' }))
})

it('should send an event to Adjust, custom mapping, App Token and Event Token mapped as properties', async () => {
nock('https://s2s.adjust.com').post('/event').reply(200, { status: 'OK' })

const goodEvent = createTestEvent({
type: 'track',
context: {
device: {
id: DEVICE_ID,
advertisingId: ADVERTISING_ID,
type: DEVICE_TYPE
},
library: {
name: 'analytics-ios',
version: '4.0.0'
}
},
properties: {
revenue: 10,
currency: 'USD',
appToken: 'app-token',
eventToken: 'event-token'
},
timestamp: '2024-07-05T12:34:56.789Z'
})

const responses = await testDestination.testAction('sendEvent', {
event: goodEvent,
mapping: {
app_token: {
'@path': '$.properties.appToken'
},
event_token: {
'@path': '$.properties.eventToken'
},
device_id: {
'@path': '$.context.device.id'
},
advertising_id: {
'@path': '$.context.device.advertisingId'
},
device_type: {
'@path': '$.context.device.type'
}
},
settings: {
environment: 'sandbox'
}
})

expect(responses).toHaveLength(1)
expect(responses[0].content).toBeDefined()
expect(responses[0].content).toEqual(JSON.stringify({ status: 'OK' }))
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 actionSlug = 'sendEvent'
const destinationSlug = 'Adjust'
const seedName = `${destinationSlug}#${actionSlug}`

describe(`Testing snapshot for ${destinationSlug}'s ${actionSlug} destination action:`, () => {
it('required fields', async () => {
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('all fields', async () => {
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()
}
})
})
Loading
Loading