Skip to content

Commit

Permalink
dv360 first party actions
Browse files Browse the repository at this point in the history
  • Loading branch information
rvadera12 committed Oct 23, 2024
1 parent 7b78fb6 commit cbb00b3
Show file tree
Hide file tree
Showing 14 changed files with 553 additions and 52 deletions.

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,26 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { AudienceSettings, Settings } from '../generated-types'
import type { Payload } from './generated-types'
import { advertiser_id, external_id, mobileDeviceIds } from '../properties'
import { editDeviceMobileIds } from '../functions'

const action: ActionDefinition<Settings, Payload, AudienceSettings> = {
title: 'Edit Customer Match Members - Mobile Device Id List',
description: 'Add or update customer match members in Google Display & Video 360 Mobile Device Id List Audience.',
defaultSubscription: 'event = "Audience Entered',
fields: {
mobileDeviceIds: { ...mobileDeviceIds },
external_id: { ...external_id },
advertiser_id: { ...advertiser_id }
},
perform: async (request, { payload, statsContext }) => {
statsContext?.statsClient?.incr('editCustomerMatchMembers', 1, statsContext?.tags)
return editDeviceMobileIds(request, [payload], 'add', statsContext)
},
performBatch: async (request, { payload, statsContext }) => {
statsContext?.statsClient?.incr('editCustomerMatchMembers.batch', 1, statsContext?.tags)
return editDeviceMobileIds(request, payload, 'add', statsContext)
}
}

export default action

This file was deleted.

This file was deleted.

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,40 @@
import type { ActionDefinition } from '@segment/actions-core'
import type { AudienceSettings, Settings } from '../generated-types'
import type { Payload } from './generated-types'
import {
emails,
phoneNumbers,
zipCodes,
firstName,
lastName,
countryCode,
external_id,
advertiser_id
} from '../properties'
import { editContactInfo } from '../functions'

const action: ActionDefinition<Settings, Payload, AudienceSettings> = {
title: 'Edit Customer Match Members - Contact Info List',
description: 'Add or update customer match members in Google Display & Video 360 Contact Info List Audience.',
defaultSubscription: 'event = "Audience Entered"',
fields: {
emails: { ...emails },
phoneNumbers: { ...phoneNumbers },
zipCodes: { ...zipCodes },
firstName: { ...firstName },
lastName: { ...lastName },
countryCode: { ...countryCode },
external_id: { ...external_id },
advertiser_id: { ...advertiser_id }
},
perform: async (request, { payload, statsContext }) => {
statsContext?.statsClient?.incr('editCustomerMatchMembers', 1, statsContext?.tags)
return editContactInfo(request, [payload], 'add', statsContext)
},
performBatch: async (request, { payload, statsContext }) => {
statsContext?.statsClient?.incr('editCustomerMatchMembers.batch', 1, statsContext?.tags)
return editContactInfo(request, payload, 'add', statsContext)
}
}

export default action
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { RequestClient } from '@segment/actions-core'
import { IntegrationError, RequestClient, StatsContext } from '@segment/actions-core'
import { Payload } from './addToAudContactInfo/generated-types'
import { createHash } from 'crypto'
import { Payload as DeviceIdPayload } from './addToAudMobileDeviceId/generated-types'

const DV360API = `https://displayvideo.googleapis.com/v3/firstAndThirdPartyAudiences`
const CONSENT_STATUS_GRANTED = 'CONSENT_STATUS_GRANTED' // Define consent status
const OAUTH_URL = 'https://accounts.google.com/o/oauth2/token'

interface createAudienceRequestParams {
advertiserId: string
Expand All @@ -18,6 +23,52 @@ interface getAudienceParams {
token?: string
}

interface DV360editCustomerMatchResponse {
firstAndThirdPartyAudienceId: string
error: [
{
code: string
message: string
status: string
}
]
}

interface RefreshTokenResponse {
access_token: string
}

type DV360AuthCredentials = { refresh_token: string; access_token: string; client_id: string; client_secret: string }

export const getAuthSettings = (): DV360AuthCredentials => {
return {
refresh_token: process.env.ACTIONS_DISPLAY_VIDEO_360_REFRESH_TOKEN,
client_id: process.env.ACTIONS_DISPLAY_VIDEO_360_CLIENT_ID,
client_secret: process.env.ACTIONS_DISPLAY_VIDEO_360_CLIENT_SECRET
} as DV360AuthCredentials
}

// Use the refresh token to get a new access token.
// Refresh tokens, Client_id and secret are long-lived and belong to the DMP.
// Given the short expiration time of access tokens, we need to refresh them periodically.
export const getAuthToken = async (request: RequestClient, settings: DV360AuthCredentials) => {
if (!settings.refresh_token) {
throw new IntegrationError('Refresh token is missing', 'INVALID_REQUEST_DATA', 400)
}

const { data } = await request<RefreshTokenResponse>(OAUTH_URL, {
method: 'POST',
body: new URLSearchParams({
refresh_token: settings.refresh_token,
client_id: settings.client_id,
client_secret: settings.client_secret,
grant_type: 'refresh_token'
})
})

return data.access_token
}

export const createAudienceRequest = (
request: RequestClient,
params: createAudienceRequestParams
Expand Down Expand Up @@ -57,3 +108,150 @@ export const getAudienceRequest = (request: RequestClient, params: getAudiencePa
}
})
}

export async function editDeviceMobileIds(
request: RequestClient,
payloads: DeviceIdPayload[],
operation: 'add' | 'remove',
statsContext?: StatsContext // Adjust type based on actual stats context
) {
const payload = payloads[0]
const audienceId = payload.external_id

//Check if mobile device id exists otherwise drop the event
if (payload.mobileDeviceIds === undefined) {
return
}

//Get access token
const authSettings = getAuthSettings()
const token = await getAuthToken(request, authSettings)

//Format the endpoint
const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers'

// Prepare the request payload
const mobileDeviceIdList = {
mobileDeviceIds: [payload.mobileDeviceIds],
consent: {
adUserData: CONSENT_STATUS_GRANTED,
adPersonalization: CONSENT_STATUS_GRANTED
}
}

// Convert the payload to string if needed
const requestPayload = JSON.stringify({
advertiserId: payload.advertiser_id,
...(operation === 'add' ? { addedMobileDeviceIdList: mobileDeviceIdList } : {}),
...(operation === 'remove' ? { removedMobileDeviceIdList: mobileDeviceIdList } : {})
})
const response = await request<DV360editCustomerMatchResponse>(endpoint, {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'Content-Type': 'application/json; charset=utf-8'
},
body: requestPayload
})
if (!response.data || !response.data.firstAndThirdPartyAudienceId) {
statsContext?.statsClient?.incr('addCustomerMatchMembers.error', 1, statsContext?.tags)
throw new IntegrationError(
`API returned error: ${response.data?.error || 'Unknown error'}`,
'API_REQUEST_ERROR',
400
)
}

statsContext?.statsClient?.incr('addCustomerMatchMembers.success', 1, statsContext?.tags)
return response.data
}

export async function editContactInfo(
request: RequestClient,
payloads: Payload[],
operation: 'add' | 'remove',
statsContext?: StatsContext
) {
const payload = payloads[0]
const audienceId = payloads[0].external_id

//Check if one of the required identifiers exists otherwise drop the event
if (
payload.emails === undefined &&
payload.phoneNumbers === undefined &&
payload.firstName === undefined &&
payload.lastName === undefined
) {
return
}

//Get access token
const authSettings = getAuthSettings()
const token = await getAuthToken(request, authSettings)

//Format the endpoint
const endpoint = DV360API + '/' + audienceId + ':editCustomerMatchMembers'

// Prepare the request payload
const contactInfoList = {
contactInfos: [processPayload(payload)],
consent: {
adUserData: CONSENT_STATUS_GRANTED,
adPersonalization: CONSENT_STATUS_GRANTED
}
}

// Convert the payload to string if needed
const requestPayload = JSON.stringify({
advertiserId: payload.advertiser_id,
...(operation === 'add' ? { addedContactInfoList: contactInfoList } : {}),
...(operation === 'remove' ? { removedContactInfoList: contactInfoList } : {})
})

const response = await request<DV360editCustomerMatchResponse>(endpoint, {
method: 'POST',
headers: {
authorization: `Bearer ${token}`,
'Content-Type': 'application/json; charset=utf-8'
},
body: requestPayload
})

statsContext?.statsClient?.incr('addCustomerMatchMembers.success', 1, statsContext?.tags)
return response.data
}

function normalizeAndHash(data: string) {
// Normalize the data
const normalizedData = data.toLowerCase().trim() // Example: Convert to lowercase and remove leading/trailing spaces
// Hash the normalized data using SHA-256
const hash = createHash('sha256')
hash.update(normalizedData)
return hash.digest('hex')
}

function processPayload(payload: Payload) {
const result: { [key: string]: string } = {}

// Normalize and hash only if the value is defined
if (payload.emails) {
result.hashedEmails = normalizeAndHash(payload.emails)
}
if (payload.phoneNumbers) {
result.hashedPhoneNumbers = normalizeAndHash(payload.phoneNumbers)
}
if (payload.zipCodes) {
result.zipCodes = payload.zipCodes
}
if (payload.firstName) {
result.hashedFirstName = normalizeAndHash(payload.firstName)
}
if (payload.lastName) {
result.hashedLastName = normalizeAndHash(payload.lastName)
}
if (payload.countryCode) {
result.countryCode = payload.countryCode
}

return result
}

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

Loading

0 comments on commit cbb00b3

Please sign in to comment.