Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Object {
"testType": "YPhGI%A",
},
"channel": "YPhGI%A",
"dsp_metadata": Object {
"testType": "YPhGI%A",
},
"entity": Object {
"id": "YPhGI%A",
"type": "YPhGI%A",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import nock from 'nock'
import { AggregateAjvError } from '@segment/ajv-human-errors'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import type { SegmentEvent } from '@segment/actions-core'
import Destination from '../../index'
Expand Down Expand Up @@ -69,20 +68,71 @@ describe('Topsort.click', () => {
})
})

it('should fail because it misses a required field (resolvedBidId)', async () => {
it('should be successful for an offsite click without resolvedBidId', async () => {
nock(/.*/).persist().post(/.*/).reply(200)

const event = createTestEvent({})
const event = createTestEvent({
properties: {
externalVendorId: '1234',
externalCampaignId: 'campaignId1234',
entity: { id: '677061', type: 'product' },
channel: 'offsite',
dspMetadata: { gclid: 'google-click-id-123' }
}
})

await expect(
testDestination.testAction('click', {
event,
settings: {
api_key: 'bar'
},
useDefaultMappings: true
})
).rejects.toThrowError(AggregateAjvError)
const responses = await testDestination.testAction('click', {
event,
settings: {
api_key: 'bar'
},
useDefaultMappings: true
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
const click = (responses[0].options.json as { clicks: Record<string, unknown>[] }).clicks[0]
expect(click).not.toHaveProperty('resolvedBidId')
expect(click).not.toHaveProperty('dspMetadata')
expect(responses[0].options.json).toMatchObject({
clicks: expect.arrayContaining([
expect.objectContaining({
externalVendorId: '1234',
externalCampaignId: 'campaignId1234',
entity: { id: '677061', type: 'product' },
channel: 'offsite',
dsp_metadata: { gclid: 'google-click-id-123' }
})
])
})
})

it('should coerce non-string dspMetadata values to strings', async () => {
nock(/.*/).persist().post(/.*/).reply(200)

const event = createTestEvent({
properties: {
resolvedBidId: 'thisisaresolvedbidid',
dspMetadata: { gclid: 'abc', score: 42, nested: { a: 1 } }
}
})

const responses = await testDestination.testAction('click', {
event,
settings: {
api_key: 'bar'
},
useDefaultMappings: true
})

expect(responses[0].status).toBe(200)
expect(responses[0].options.json).toMatchObject({
clicks: expect.arrayContaining([
expect.objectContaining({
dsp_metadata: { gclid: 'abc', score: '42', nested: '{"a":1}' }
})
])
})
})

it('should be successful with new optional fields', async () => {
Expand Down

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
Expand Up @@ -2,7 +2,7 @@ import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { TopsortAPIClient } from '../client'
import { NormalizeDeviceType } from '../functions'
import { NormalizeDeviceType, NormalizeDspMetadata } from '../functions'

const action: ActionDefinition<Settings, Payload> = {
title: 'Click',
Expand Down Expand Up @@ -41,9 +41,9 @@ const action: ActionDefinition<Settings, Payload> = {
resolvedBidId: {
label: 'Resolved Bid ID',
description:
'Identifier of an instance of a resolved auction for a determined product. The length should not exceed 128 characters.',
'Identifier of an instance of a resolved auction for a determined product. The length should not exceed 128 characters. Required for onsite sponsored events; omit for offsite or organic events, which are attributed via entity, externalCampaignId/externalVendorId and channel instead.',
type: 'string',
required: true,
required: false,
default: {
'@path': '$.properties.resolvedBidId'
}
Expand Down Expand Up @@ -264,14 +264,29 @@ const action: ActionDefinition<Settings, Payload> = {
default: {
'@path': '$.properties.channel'
}
},
dspMetadata: {
label: 'DSP Metadata',
description:
'Metadata used to forward click identifiers to the DSP for offsite conversions (e.g. { "gclid": "..." } for Google, or the equivalent click identifier for Meta). The accepted keys depend on the advertising platform. Values must be strings (the API expects a map of string to string); non-string values are JSON-stringified before sending. Typically only set for offsite events.',
type: 'object',
required: false,
default: {
'@path': '$.properties.dspMetadata'
}
}
},
perform: (request, { payload, settings }) => {
const client = new TopsortAPIClient(request, settings)

payload.deviceType = NormalizeDeviceType(payload.deviceType)
const { dspMetadata, ...rest } = payload
const click = {
...rest,
deviceType: NormalizeDeviceType(payload.deviceType),
dsp_metadata: NormalizeDspMetadata(dspMetadata)
}
return client.sendEvent({
clicks: [payload]
clicks: [click]
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,21 @@ export function NormalizeDeviceType(t: string | undefined): string | undefined {
}
return t
}

// The Report Events API expects dsp_metadata to be a map of string to string. Coerce any non-string
// values to strings so a single number/object/array value doesn't cause the whole batch to be rejected.
export function NormalizeDspMetadata(
metadata: { [k: string]: unknown } | undefined
): { [k: string]: string } | undefined {
if (!metadata) {
return undefined
}
const normalized: { [k: string]: string } = {}
for (const [key, value] of Object.entries(metadata)) {
if (value === undefined || value === null) {
continue
}
normalized[key] = typeof value === 'string' ? value : JSON.stringify(value)
}
return Object.keys(normalized).length > 0 ? normalized : undefined
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Object {
"testType": "13(ufD",
},
"channel": "13(ufD",
"dsp_metadata": Object {
"testType": "13(ufD",
},
"entity": Object {
"id": "13(ufD",
"type": "13(ufD",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import nock from 'nock'
import { AggregateAjvError } from '@segment/ajv-human-errors'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import type { SegmentEvent } from '@segment/actions-core'
import Destination from '../../index'
Expand Down Expand Up @@ -232,20 +231,71 @@ describe('Topsort.impression', () => {
})
})

it('should fail because it misses a required field (resolvedBidId)', async () => {
it('should be successful for an offsite impression without resolvedBidId', async () => {
nock(/.*/).persist().post(/.*/).reply(200)

const event = createTestEvent({})
const event = createTestEvent({
properties: {
externalVendorId: '1234',
externalCampaignId: 'campaignId1234',
entity: { id: '677061', type: 'product' },
channel: 'offsite',
dspMetadata: { gclid: 'google-click-id-123' }
}
})

await expect(
testDestination.testAction('impression', {
event,
settings: {
api_key: 'bar'
},
useDefaultMappings: true
})
).rejects.toThrowError(AggregateAjvError)
const responses = await testDestination.testAction('impression', {
event,
settings: {
api_key: 'bar'
},
useDefaultMappings: true
})

expect(responses.length).toBe(1)
expect(responses[0].status).toBe(200)
const impression = (responses[0].options.json as { impressions: Record<string, unknown>[] }).impressions[0]
expect(impression).not.toHaveProperty('resolvedBidId')
expect(impression).not.toHaveProperty('dspMetadata')
expect(responses[0].options.json).toMatchObject({
impressions: expect.arrayContaining([
expect.objectContaining({
externalVendorId: '1234',
externalCampaignId: 'campaignId1234',
entity: { id: '677061', type: 'product' },
channel: 'offsite',
dsp_metadata: { gclid: 'google-click-id-123' }
Comment thread
barbmarcio marked this conversation as resolved.
})
])
})
})

it('should coerce non-string dspMetadata values to strings', async () => {
nock(/.*/).persist().post(/.*/).reply(200)

const event = createTestEvent({
properties: {
resolvedBidId: 'thisisaresolvedbidid',
dspMetadata: { gclid: 'abc', score: 42, nested: { a: 1 } }
}
})

const responses = await testDestination.testAction('impression', {
event,
settings: {
api_key: 'bar'
},
useDefaultMappings: true
})

expect(responses[0].status).toBe(200)
expect(responses[0].options.json).toMatchObject({
impressions: expect.arrayContaining([
expect.objectContaining({
dsp_metadata: { gclid: 'abc', score: '42', nested: '{"a":1}' }
})
])
})
})
})

Expand Down

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
Expand Up @@ -2,7 +2,7 @@ import type { ActionDefinition } from '@segment/actions-core'
import type { Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { TopsortAPIClient } from '../client'
import { NormalizeDeviceType } from '../functions'
import { NormalizeDeviceType, NormalizeDspMetadata } from '../functions'

const action: ActionDefinition<Settings, Payload> = {
title: 'Impression',
Expand Down Expand Up @@ -41,9 +41,9 @@ const action: ActionDefinition<Settings, Payload> = {
resolvedBidId: {
label: 'Resolved Bid ID',
description:
'Identifier of an instance of a resolved auction for a determined product. The length should not exceed 128 characters.',
'Identifier of an instance of a resolved auction for a determined product. The length should not exceed 128 characters. Required for onsite sponsored events; omit for offsite or organic events, which are attributed via entity, externalCampaignId/externalVendorId and channel instead.',
type: 'string',
required: true,
required: false,
default: {
'@path': '$.properties.resolvedBidId'
}
Expand Down Expand Up @@ -264,14 +264,29 @@ const action: ActionDefinition<Settings, Payload> = {
default: {
'@path': '$.properties.channel'
}
},
dspMetadata: {
label: 'DSP Metadata',
description:
'Metadata used to forward click identifiers to the DSP for offsite conversions (e.g. { "gclid": "..." } for Google, or the equivalent click identifier for Meta). The accepted keys depend on the advertising platform. Values must be strings (the API expects a map of string to string); non-string values are JSON-stringified before sending. Typically only set for offsite events.',
type: 'object',
required: false,
default: {
'@path': '$.properties.dspMetadata'
}
}
},
perform: (request, { payload, settings }) => {
const client = new TopsortAPIClient(request, settings)

payload.deviceType = NormalizeDeviceType(payload.deviceType)
const { dspMetadata, ...rest } = payload
const impression = {
...rest,
deviceType: NormalizeDeviceType(payload.deviceType),
dsp_metadata: NormalizeDspMetadata(dspMetadata)
}
return client.sendEvent({
impressions: [payload]
impressions: [impression]
})
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ Object {
"testType": "13(ufD",
},
"channel": "13(ufD",
"dsp_metadata": Object {
"testType": "13(ufD",
},
"entity": Object {
"id": "13(ufD",
"type": "13(ufD",
Expand Down