Skip to content

Commit

Permalink
Adjust Action Destination: First implementation + unit tests. (#2144)
Browse files Browse the repository at this point in the history
* First implementation + unit tests.

* Not using `rawData` in the type. That breaks the build.

* Just not checking `data` for a `rawData` property.

* Update packages/destination-actions/src/destinations/adjust/index.ts

Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com>

* Update packages/destination-actions/src/destinations/adjust/functions.ts

Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com>

* Update packages/destination-actions/src/destinations/adjust/functions.ts

Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com>

* Modifications requested in PR review.

* Unit test adjustments.

* Not using snapshot tests.

* Updating generated types.

* ready for deploy

---------

Co-authored-by: Joe Ayoub <45374896+joe-ayoub-segment@users.noreply.github.com>
Co-authored-by: Joe Ayoub <joe.ayoub@segment.com>
  • Loading branch information
3 people authored and marinhero committed Aug 2, 2024
1 parent 1838ade commit bb89fc7
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 0 deletions.
53 changes: 53 additions & 0 deletions packages/destination-actions/src/destinations/adjust/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { RequestClient } from '@segment/actions-core/create-request-client'
import { IntegrationError } from '@segment/actions-core'
import { AdjustPayload } from './types'
import { Settings } from './generated-types'
import { Payload } from './sendEvent/generated-types'

export function validatePayload(payload: Payload, settings: Settings): AdjustPayload {
if (!payload.app_token && !settings.default_app_token) {
throw new IntegrationError(
'One of app_token field or default_app_token setting fields must have a value.',
'APP_TOKEN_VALIDATION_FAILED',
400
)
}

if (!payload.event_token && !settings.default_event_token) {
throw new IntegrationError(
'One of event_token field or default_event_token setting fields must have a value.',
'EVENT_TOKEN_VALIDATION_FAILED',
400
)
}

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),
created_at_unix: payload.timestamp
? parseInt((new Date(String(payload.timestamp)).getTime() / 1000).toFixed(0))
: undefined
}

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[]) {
return await Promise.all(
events.map((event) =>
request('https://s2s.adjust.com/event', {
method: 'POST',
body: JSON.stringify(event)
})
)
)
}

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

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

import sendEvent from './sendEvent'

const destination: DestinationDefinition<Settings> = {
name: 'Adjust (Actions)',
slug: 'actions-adjust',
mode: 'cloud',
description: 'Send events to Adjust.',
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. This can be overridden in the event mapping.',
type: 'string',
required: false
},
default_event_token: {
label: 'Default Event Token',
description: 'The default event token. This can be overridden in the event mapping.',
type: 'string',
required: false
}
}
},

actions: {
sendEvent
}
}

export default destination
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
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'
}
})

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

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'
}
})

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' }))
})
})
})

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

const action: ActionDefinition<Settings, Payload> = {
title: 'Send Event',
description: 'Sends an Event to Adjust.',
defaultSubscription: 'type = "track" and event = "Conversion Completed"',
fields: {
timestamp: {
label: 'Timestamp',
description: 'Timestamp for when the event happened.',
type: 'datetime',
default: {
'@path': '$.timestamp'
},
required: false
},
app_token: {
label: 'App Token',
description: 'The app token for your Adjust account. Overrides the Default App Token from Settings.',
type: 'string',
required: false
},
event_token: {
label: 'Event Token',
description: 'The event token. Overrides the Default Event Token from Settings.',
type: 'string',
required: false
},
device_id: {
label: 'Device ID',
description: 'The unique device identifier',
type: 'string',
default: {
'@path': '$.context.device.id'
},
required: true
},
advertising_id: {
label: 'Advertising ID',
description: 'The advertising identifier ("idfa" for iOS, "gps_adid" for Android).',
type: 'string',
default: {
'@path': '$.context.device.advertisingId'
},
required: true
},
device_type: {
label: 'Device Type',
description: 'The device type. Options: "ios" or "android".',
type: 'string',
default: {
'@path': '$.context.device.type'
},
required: true
},
library_name: {
label: 'Library Name',
description:
'The name of the Segment library used to trigger the event. E.g. "analytics-ios" or "analytics-android".',
type: 'string',
default: {
'@path': '$.context.library.name'
},
required: false
},
revenue: {
label: 'Revenue',
description:
'The revenue amount of the event. E.g. 75.5 for $75.50. Currency can be set with the "Currency field".',
type: 'number',
required: false,
default: { '@path': '$.properties.revenue' }
},
currency: {
label: 'Currency',
description: 'The revenue currency. Only set if revenue is also set. E.g. "USD" or "EUR".',
type: 'string',
required: false,
default: { '@path': '$.properties.currency' }
}
},
perform: (request, data) => {
const adjustPayload = validatePayload(data.payload, data.settings)
return sendEvents(request, [adjustPayload])
}
}

export default action
8 changes: 8 additions & 0 deletions packages/destination-actions/src/destinations/adjust/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface AdjustPayload {
app_token: string
event_token: string
environment: string
s2s: number
callback_params?: string
created_at_unix?: number
}

0 comments on commit bb89fc7

Please sign in to comment.