Skip to content

Introduce 'Amazon Conversions Api' Destination #2941

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

Merged
merged 15 commits into from
Jun 3, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Basic test to confirm destination structure
import Destination from '../index'
import type { DestinationDefinition } from '@segment/actions-core'

describe('Amazon Conversions API Destination', () => {
it('should be properly configured', () => {
// Test destination structure
expect(Destination).toHaveProperty('name', 'Amazon Conversions Api')
expect(Destination).toHaveProperty('slug', 'amazon-conversions-api')
expect(Destination).toHaveProperty('mode', 'cloud')

// Test authentication exists
expect(Destination).toHaveProperty('authentication')
expect(Destination.authentication).toHaveProperty('scheme', 'oauth2')

// Test actions exist
expect(Destination).toHaveProperty('actions')
expect(Destination.actions).toHaveProperty('trackConversion')
})

it('should have trackConversion action', () => {
expect(typeof Destination.actions.trackConversion).toBe('object')
expect(Destination.actions.trackConversion).toHaveProperty('title', 'Track Conversion')
expect(Destination.actions.trackConversion).toHaveProperty('perform')
expect(typeof Destination.actions.trackConversion.perform).toBe('function')
})

describe('Presets', () => {
it('should have presets defined', () => {
expect(Destination).toHaveProperty('presets')
const presets = (Destination as DestinationDefinition<any>).presets
expect(Array.isArray(presets)).toBe(true)
expect(presets?.length).toBeGreaterThan(0)
})

it('should have properly configured presets', () => {
// Get presets from destination
const presets = (Destination as DestinationDefinition<any>).presets || []

// Check some key presets exist
const addToCartPreset = presets.find(p => p.name === 'Add to Shopping Cart')
const pageViewPreset = presets.find(p => p.name === 'Page View')
const signUpPreset = presets.find(p => p.name === 'Sign Up')

expect(addToCartPreset).toBeDefined()
expect(pageViewPreset).toBeDefined()
expect(signUpPreset).toBeDefined()

// Check preset configurations
expect(addToCartPreset?.partnerAction).toBe('trackConversion')
expect(addToCartPreset?.mapping?.eventType).toBe('ADD_TO_SHOPPING_CART')
expect((addToCartPreset as any)?.subscribe).toBe('type = "track" AND event = "Product Added"')

expect(pageViewPreset?.partnerAction).toBe('trackConversion')
expect(pageViewPreset?.mapping?.eventType).toBe('PAGE_VIEW')
expect((pageViewPreset as any)?.subscribe).toBe('type = "page"')

expect(signUpPreset?.partnerAction).toBe('trackConversion')
expect(signUpPreset?.mapping?.eventType).toBe('SIGN_UP')
expect((signUpPreset as any)?.subscribe).toBe('type = "track" AND event = "Signed Up"')
})

it('should have presets for all supported event types', () => {
const presets = (Destination as DestinationDefinition<any>).presets || []

// Get all event types from the presets
const presetEventTypes = presets.map(p => p.mapping?.eventType)

// Check that all expected Amazon event types are covered
expect(presetEventTypes).toContain('ADD_TO_SHOPPING_CART')
expect(presetEventTypes).toContain('APPLICATION')
expect(presetEventTypes).toContain('CHECKOUT')
expect(presetEventTypes).toContain('CONTACT')
expect(presetEventTypes).toContain('LEAD')
expect(presetEventTypes).toContain('OFF_AMAZON_PURCHASES')
expect(presetEventTypes).toContain('MOBILE_APP_FIRST_START')
expect(presetEventTypes).toContain('PAGE_VIEW')
expect(presetEventTypes).toContain('SEARCH')
expect(presetEventTypes).toContain('SIGN_UP')
expect(presetEventTypes).toContain('SUBSCRIBE')
})

it('should map Segment events to the correct Amazon event types', () => {
const presets = (Destination as DestinationDefinition<any>).presets || []

const eventMapping = {
'Product Added': 'ADD_TO_SHOPPING_CART',
'Application Submitted': 'APPLICATION',
'Checkout Started': 'CHECKOUT',
'Callback Started': 'CONTACT',
'Lead Generated': 'LEAD',
'Order Completed': 'OFF_AMAZON_PURCHASES',
'Application Opened': 'MOBILE_APP_FIRST_START',
'page': 'PAGE_VIEW', // For type="page"
'Products Searched': 'SEARCH',
'Signed Up': 'SIGN_UP',
'Subscription Created': 'SUBSCRIBE'
}

// Check each mapping has a preset
for (const [segmentEvent, amazonEvent] of Object.entries(eventMapping)) {
let preset

if (segmentEvent === 'page') {
// Special case for page type
preset = presets.find(p => (p as any)?.subscribe === 'type = "page"')
} else {
preset = presets.find(p => (p as any)?.subscribe?.includes(`event = "${segmentEvent}"`))
}

expect(preset).toBeDefined()
expect(preset?.mapping?.eventType).toBe(amazonEvent)
}
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Region,
ConversionTypeV2,
CurrencyCodeV1,
MatchKeyTypeV1
} from '../types'

describe('Amazon Conversions API Types', () => {
describe('Region enum', () => {
it('should have the correct API endpoints defined', () => {
expect(Region.NA).toBe('https://advertising-api.amazon.com')
expect(Region.EU).toBe('https://advertising-api-eu.amazon.com')
expect(Region.FE).toBe('https://advertising-api-fe.amazon.com')
})
})

describe('ConversionTypeV2 enum', () => {
it('should have all expected event types defined', () => {
expect(ConversionTypeV2.ADD_TO_SHOPPING_CART).toBe('ADD_TO_SHOPPING_CART')
expect(ConversionTypeV2.APPLICATION).toBe('APPLICATION')
expect(ConversionTypeV2.CHECKOUT).toBe('CHECKOUT')
expect(ConversionTypeV2.CONTACT).toBe('CONTACT')
expect(ConversionTypeV2.LEAD).toBe('LEAD')
expect(ConversionTypeV2.OFF_AMAZON_PURCHASES).toBe('OFF_AMAZON_PURCHASES')
expect(ConversionTypeV2.MOBILE_APP_FIRST_START).toBe('MOBILE_APP_FIRST_START')
expect(ConversionTypeV2.PAGE_VIEW).toBe('PAGE_VIEW')
expect(ConversionTypeV2.SEARCH).toBe('SEARCH')
expect(ConversionTypeV2.SIGN_UP).toBe('SIGN_UP')
expect(ConversionTypeV2.SUBSCRIBE).toBe('SUBSCRIBE')
expect(ConversionTypeV2.OTHER).toBe('OTHER')
})
})

describe('CurrencyCodeV1 enum', () => {
it('should have common currency codes defined', () => {
expect(CurrencyCodeV1.USD).toBe('USD')
expect(CurrencyCodeV1.EUR).toBe('EUR')
expect(CurrencyCodeV1.GBP).toBe('GBP')
expect(CurrencyCodeV1.JPY).toBe('JPY')
expect(CurrencyCodeV1.CAD).toBe('CAD')
expect(CurrencyCodeV1.AUD).toBe('AUD')
})
})

describe('MatchKeyTypeV1 enum', () => {
it('should have all match key types defined', () => {
expect(MatchKeyTypeV1.EMAIL).toBe('EMAIL')
expect(MatchKeyTypeV1.PHONE).toBe('PHONE')
expect(MatchKeyTypeV1.FIRST_NAME).toBe('FIRST_NAME')
expect(MatchKeyTypeV1.LAST_NAME).toBe('LAST_NAME')
expect(MatchKeyTypeV1.ADDRESS).toBe('ADDRESS')
expect(MatchKeyTypeV1.CITY).toBe('CITY')
expect(MatchKeyTypeV1.STATE).toBe('STATE')
expect(MatchKeyTypeV1.POSTAL).toBe('POSTAL')
expect(MatchKeyTypeV1.MAID).toBe('MAID')
expect(MatchKeyTypeV1.RAMP_ID).toBe('RAMP_ID')
expect(MatchKeyTypeV1.MATCH_ID).toBe('MATCH_ID')
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { getAuthToken } from '../utils'
import nock from 'nock'
import { OAuth2ClientCredentials, RequestClient } from '@segment/actions-core'

describe('Amazon Conversions API Utils', () => {
beforeEach(() => {
nock.cleanAll()
})

describe('getAuthToken', () => {
it('should successfully refresh access token', async () => {
const mockAccessToken = 'new-access-token-123'
const auth = { refreshToken: 'test-refresh-token', accessToken: 'old-token' }

nock('https://api.amazon.com')
.post('/auth/o2/token')
.reply(200, { access_token: mockAccessToken })

// Create mock request function that returns the expected response
const mockRequest = jest.fn().mockResolvedValue({
data: { access_token: mockAccessToken }
})

const token = await getAuthToken(mockRequest as RequestClient, auth as OAuth2ClientCredentials)
expect(token).toBe(mockAccessToken)

// Verify the request was made with correct parameters
expect(mockRequest).toHaveBeenCalled()
expect(mockRequest.mock.calls[0][0]).toBe('https://api.amazon.com/auth/o2/token')

// Safe access to call arguments
const callArg1 = mockRequest.mock.calls[0][1]
expect(callArg1).toBeDefined()
expect(callArg1.method).toBe('POST')
expect(callArg1.headers).toBeDefined()
expect(callArg1.headers.authorization).toBe('')

// Safely check URLSearchParams
expect(callArg1.body).toBeInstanceOf(URLSearchParams)
const bodyParams = callArg1.body.toString()
expect(bodyParams).toContain('grant_type=refresh_token')
expect(bodyParams).toContain(`refresh_token=${auth.refreshToken}`)
})

it('should handle missing environment variables', async () => {
// Save original env vars
const originalClientId = process.env.ACTIONS_AMAZON_CONVERSIONS_API_CLIENT_ID
const originalClientSecret = process.env.ACTIONS_AMAZON_CONVERSIONS_API_CLIENT_SECRET

try {
// Remove env vars to test handling of missing values
delete process.env.ACTIONS_AMAZON_CONVERSIONS_API_CLIENT_ID
delete process.env.ACTIONS_AMAZON_CONVERSIONS_API_CLIENT_SECRET

const auth = { refreshToken: 'test-refresh-token', accessToken: 'old-token' }

// Create mock request function with successful response
const mockRequest = jest.fn().mockResolvedValue({
data: { access_token: 'new-token' }
})

await getAuthToken(mockRequest as RequestClient, auth as OAuth2ClientCredentials)

// Check that request was called
expect(mockRequest).toHaveBeenCalled()

// Safely check call arguments
const callArg1 = mockRequest.mock.calls[0][1]
expect(callArg1).toBeDefined()
expect(callArg1.body).toBeInstanceOf(URLSearchParams)

// Check URLSearchParams content
const bodyParams = callArg1.body.toString()
expect(bodyParams).toContain('client_id=')
expect(bodyParams).toContain('client_secret=')
} finally {
// Ensure env vars are always restored
process.env.ACTIONS_AMAZON_CONVERSIONS_API_CLIENT_ID = originalClientId
process.env.ACTIONS_AMAZON_CONVERSIONS_API_CLIENT_SECRET = originalClientSecret
}
})
})
})

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

Loading
Loading