Skip to content

Stratconn 5870 google data manager #3010

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
@@ -1,9 +1,69 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Testing snapshot for actions-google-data-manager destination: ingest action - all fields 1`] = `Array []`;
exports[`Testing snapshot for actions-google-data-manager destination: ingest action - all fields 1`] = `
Object {
"audienceMembers": Array [
Object {
"consent": Object {
"adPersonalization": "CONSENT_GRANTED",
"adUserData": "CONSENT_GRANTED",
},
"userData": Object {
"userIdentifiers": Array [
Object {
"address": Object {
"familyName": "ZWTB^0A]@#Be(",
"givenName": "ZWTB^0A]@#Be(",
"postalCode": "ZWTB^0A]@#Be(",
"regionCode": "ZWTB^0A]@#Be(",
},
"emailAddress": "ZWTB^0A]@#Be(",
"phoneNumber": "ZWTB^0A]@#Be(",
},
],
},
},
],
"destinations": Array [
Object {
"loginAccount": Object {
"accountId": "8172370552",
"product": "DATA_PARTNER",
},
"operatingAccount": Object {},
},
],
"encoding": "BASE64",
}
`;

exports[`Testing snapshot for actions-google-data-manager destination: ingest action - required fields 1`] = `Array []`;

exports[`Testing snapshot for actions-google-data-manager destination: remove action - all fields 1`] = `Array []`;

exports[`Testing snapshot for actions-google-data-manager destination: remove action - required fields 1`] = `Array []`;
exports[`Testing snapshot for actions-google-data-manager destination: ingest action - required fields 1`] = `
Object {
"audienceMembers": Array [
Object {
"consent": Object {
"adPersonalization": "CONSENT_GRANTED",
"adUserData": "CONSENT_GRANTED",
},
"userData": Object {
"userIdentifiers": Array [
Object {
"address": Object {},
"emailAddress": "ZWTB^0A]@#Be(",
},
],
},
},
],
"destinations": Array [
Object {
"loginAccount": Object {
"accountId": "8172370552",
"product": "DATA_PARTNER",
},
"operatingAccount": Object {},
},
],
"encoding": "BASE64",
}
`;
Original file line number Diff line number Diff line change
@@ -1,58 +1,11 @@
import destination from '../index'
import nock from 'nock'

describe('Google Data Manager Destination', () => {
describe('authentication', () => {
it('should have oauth2 scheme', () => {
expect(destination.authentication?.scheme).toBe('oauth2')
})

it('should have a refreshAccessToken function', () => {
expect(typeof (destination.authentication && (destination.authentication as any).refreshAccessToken)).toBe(
'function'
)
})

it('refreshAccessToken should return accessToken from response', async () => {
const mockRequest = jest.fn().mockResolvedValue({ data: { access_token: 'abc123' } })
// @ts-expect-error: refreshAccessToken is not on all auth types
const result = await destination.authentication?.refreshAccessToken?.(mockRequest, {
auth: { refreshToken: 'r', clientId: 'c', clientSecret: 's' }
})
expect(result).toEqual({ accessToken: 'abc123' })
})
})

describe('extendRequest', () => {
it('should return headers with Bearer token', () => {
const auth = { accessToken: 'token123', refreshToken: 'r', clientId: 'c', clientSecret: 's' }
const req = destination.extendRequest?.({ auth })
expect(req?.headers?.authorization).toBe('Bearer token123')
})
})

describe('audienceConfig', () => {
it('should have mode type synced and full_audience_sync false', () => {
expect(destination.audienceConfig.mode.type).toBe('synced')
// @ts-expect-error: full_audience_sync may not exist on all mode types
expect(destination.audienceConfig.mode.full_audience_sync).toBe(false)
})

it('createAudience should return an object with externalId', async () => {
// @ts-expect-error: createAudience may not exist on all configs
const result = await destination.audienceConfig.createAudience?.({}, {})
expect(result).toHaveProperty('externalId')
})

it('getAudience should return an object with externalId', async () => {
// @ts-expect-error: getAudience may not exist on all configs
const result = await destination.audienceConfig.getAudience?.({}, {})
expect(result).toHaveProperty('externalId')
})
describe('Google Data Manager - testAuthentication', () => {
afterEach(() => {
nock.cleanAll()
})

describe('onDelete', () => {
it('should be a function', () => {
expect(typeof destination.onDelete).toBe('function')
})
it('should succeed with valid access token and productLink', async () => {
return
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
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)
nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)

const event = createTestEvent({
properties: eventData
Expand All @@ -25,15 +25,9 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
event: event,
mapping: event.properties,
settings: settingsData,
auth: undefined
auth: { accessToken: 'test-access-token', refreshToken: 'test-refresh-token' }
})

// Defensive: handle case where no request is made
if (!responses[0] || !responses[0].request) {
expect(responses).toMatchSnapshot()
return
}

const request = responses[0].request
const rawBody = await request.text()

Expand All @@ -53,9 +47,9 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
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)
nock(/.*/).persist().get(/.*/).reply(200)
nock(/.*/).persist().post(/.*/).reply(200)
nock(/.*/).persist().put(/.*/).reply(200)

const event = createTestEvent({
properties: eventData
Expand All @@ -65,15 +59,9 @@ describe(`Testing snapshot for ${destinationSlug} destination:`, () => {
event: event,
mapping: event.properties,
settings: settingsData,
auth: undefined
auth: { accessToken: 'test-access-token', refreshToken: 'test-refresh-token' }
})

// Defensive: handle case where no request is made
if (!responses[0] || !responses[0].request) {
expect(responses).toMatchSnapshot()
return
}

const request = responses[0].request
const rawBody = await request.text()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const GOOGLE_API_VERSION = 'v2'
// accountType and advertiserID are used as markers to be replaced in the code. DO NOT REMOVE THEM.
// export const BASE_URL = `https://audiencepartner.googleapis.com/${GOOGLE_API_VERSION}/products/accountType/customers/advertiserID/`
export const BASE_URL = `https://audiencepartner.googleapis.com/${GOOGLE_API_VERSION}/products/GOOGLE_ADS/customers/advertiserID/` //TODO
export const CREATE_AUDIENCE_URL = `${BASE_URL}userLists:mutate`
export const GET_AUDIENCE_URL = `${BASE_URL}audiencePartner:searchStream`
export const OAUTH_URL = 'https://accounts.google.com/o/oauth2/token'
export const SEGMENT_DMP_ID = '8152223833'
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ErrorCodes, IntegrationError, InvalidAuthenticationError } from '@segment/actions-core'
import { StatsContext } from '@segment/actions-core/destination-kit'
import { GoogleAPIError } from './types'

const isGoogleAPIError = (error: unknown): error is GoogleAPIError => {
if (typeof error === 'object' && error !== null) {
const e = error as GoogleAPIError
// Not using any forces us to check for all the properties we need.
return (
typeof e.response === 'object' &&
e.response !== null &&
typeof e.response.data === 'object' &&
e.response.data !== null &&
typeof e.response.data.error === 'object' &&
e.response.data.error !== null
)
}
return false
}

// This method follows the retry logic defined in IntegrationError in the actions-core package
export const handleRequestError = (error: unknown, statsName: string, statsContext: StatsContext | undefined) => {
const { statsClient, tags: statsTags } = statsContext || {}

if (!isGoogleAPIError(error)) {
if (!error) {
statsTags?.push('error:unknown')
statsClient?.incr(`${statsName}.error`, 1, statsTags)
return new IntegrationError('Unknown error', 'UNKNOWN_ERROR', 500)
}
}

const gError = error as GoogleAPIError
const code = gError.response?.status

// @ts-ignore - Errors can be objects or arrays of objects. This will work for both.
const message = gError.response?.data?.error?.message || gError.response?.data?.[0]?.error?.message

if (code === 401) {
statsTags?.push('error:invalid-authentication')
statsClient?.incr(`${statsName}.error`, 1, statsTags)
return new InvalidAuthenticationError(message, ErrorCodes.INVALID_AUTHENTICATION)
}

if (code === 403) {
statsTags?.push('error:forbidden')
statsClient?.incr(`${statsName}.error`, 1, statsTags)
return new IntegrationError(message, 'FORBIDDEN', 403)
}

if (code === 501) {
statsTags?.push('error:integration-error')
statsClient?.incr(`${statsName}.error`, 1, statsTags)
return new IntegrationError(message, 'INTEGRATION_ERROR', 501)
}

if (code === 408 || code === 423 || code === 429 || code >= 500) {
statsTags?.push('error:retryable-error')
statsClient?.incr(`${statsName}.error`, 1, statsTags)
return new IntegrationError(message, 'RETRYABLE_ERROR', code)
}

statsTags?.push('error:generic-error')
statsClient?.incr(`${statsName}.error`, 1, statsTags)
return new IntegrationError(message, 'INTEGRATION_ERROR', code)
}

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

Loading
Loading