-
Notifications
You must be signed in to change notification settings - Fork 240
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #10 from StackAdapt/jc/onDelete-handle
[ITE-146] Update Segment Engage Destination to handle onDelete events
- Loading branch information
Showing
6 changed files
with
324 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
182 changes: 182 additions & 0 deletions
182
...on-actions/src/destinations/stackadapt-audiences/deleteProfile/__tests__/onDelete.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import nock from 'nock' | ||
import { createTestEvent, createTestIntegration } from '@segment/actions-core' | ||
import Definition from '../../index' | ||
import { SegmentEvent } from '@segment/actions-core/*' | ||
|
||
const testDestination = createTestIntegration(Definition) | ||
const mockGqlKey = 'test-graphql-key' | ||
|
||
const gqlHostUrl = 'https://api.stackadapt.com' | ||
const gqlPath = '/graphql' | ||
const mockUserId = 'user-id' | ||
const mockAdvertiserId = '23' | ||
const mockMappings = { advertiser_id: mockAdvertiserId } | ||
|
||
const deleteEventPayload: Partial<SegmentEvent> = { | ||
userId: mockUserId, | ||
type: 'identify', | ||
context: { | ||
personas: { | ||
computation_class: 'audience', | ||
computation_key: 'first_time_buyer', | ||
computation_id: 'aud_123' | ||
} | ||
} | ||
} | ||
|
||
// Helper function to mock the token query response | ||
const mockTokenQueryResponse = (nodes: Array<{ advertiser: { id: string } }>) => { | ||
nock(gqlHostUrl) | ||
.post(gqlPath) | ||
.reply(200, { | ||
data: { | ||
tokenInfo: { | ||
scopesByAdvertiser: { | ||
nodes: nodes | ||
} | ||
} | ||
} | ||
}) | ||
} | ||
|
||
// Helper function to mock the profile deletion mutation | ||
const mockDeleteProfilesMutation = ( | ||
deleteRequestBodyRef: { body?: any }, | ||
userErrors: Array<{ message: string }> = [] | ||
) => { | ||
nock(gqlHostUrl) | ||
.post(gqlPath, (body) => { | ||
deleteRequestBodyRef.body = body | ||
return body | ||
}) | ||
.reply(200, { | ||
data: { | ||
deleteProfilesWithExternalIds: { | ||
userErrors: userErrors | ||
} | ||
} | ||
}) | ||
} | ||
// helper for expected delete profiles mutation | ||
const expectDeleteProfilesMutation = ( | ||
deleteRequestBody: { body?: any }, | ||
expectedExternalIds: string[], | ||
expectedAdvertiserIds: string[] | ||
) => { | ||
expect(deleteRequestBody.body).toMatchInlineSnapshot(` | ||
Object { | ||
"query": "mutation { | ||
deleteProfilesWithExternalIds( | ||
externalIds: [\\"${expectedExternalIds.join('\\", \\"')}\\"], | ||
advertiserIDs: [\\"${expectedAdvertiserIds.join('\\", \\"')}\\"], | ||
externalProvider: \\"segmentio\\" | ||
) { | ||
userErrors { | ||
message | ||
path | ||
} | ||
} | ||
}", | ||
} | ||
`) | ||
} | ||
|
||
describe('onDelete action', () => { | ||
afterEach(() => { | ||
nock.cleanAll() | ||
}) | ||
|
||
it('should delete a profile successfully', async () => { | ||
const deleteRequestBody: { body?: any } = {} | ||
|
||
mockTokenQueryResponse([{ advertiser: { id: mockAdvertiserId } }]) | ||
mockDeleteProfilesMutation(deleteRequestBody) | ||
|
||
const event = createTestEvent({ | ||
userId: mockUserId, | ||
type: 'identify', | ||
context: { | ||
personas: { | ||
computation_class: 'audience', | ||
computation_key: 'first_time_buyer', | ||
computation_id: 'aud_123' | ||
} | ||
} | ||
}) | ||
|
||
const responses = await testDestination.testAction('onDelete', { | ||
event, | ||
useDefaultMappings: true, | ||
mapping: { userId: mockUserId }, | ||
settings: { apiKey: mockGqlKey } | ||
}) | ||
|
||
expect(responses.length).toBe(2) | ||
expectDeleteProfilesMutation(deleteRequestBody, ['user-id'], ['23']) | ||
}) | ||
|
||
it('should throw error if no advertiser ID is found', async () => { | ||
mockTokenQueryResponse([]) // Pass an empty array to mock no advertiser IDs | ||
|
||
const event = createTestEvent(deleteEventPayload) | ||
|
||
await expect( | ||
testDestination.testAction('onDelete', { | ||
event, | ||
useDefaultMappings: true, | ||
mapping: mockMappings, | ||
settings: { apiKey: mockGqlKey } | ||
}) | ||
).rejects.toThrow('No advertiser ID found.') | ||
}) | ||
|
||
it('should throw error if profile deletion fails', async () => { | ||
const deleteRequestBody: { body?: any } = {} | ||
|
||
mockTokenQueryResponse([{ advertiser: { id: mockAdvertiserId } }]) | ||
mockDeleteProfilesMutation(deleteRequestBody, [{ message: 'Deletion failed' }]) | ||
|
||
const event = createTestEvent(deleteEventPayload) | ||
|
||
await expect( | ||
testDestination.testAction('onDelete', { | ||
event, | ||
useDefaultMappings: true, | ||
mapping: mockMappings, | ||
settings: { apiKey: mockGqlKey } | ||
}) | ||
).rejects.toThrow('Profile deletion was not successful: Deletion failed') | ||
}) | ||
|
||
it('should perform onDelete with a userID and two advertiserIDs from a single token request', async () => { | ||
const deleteRequestBody: { body?: any } = {} | ||
|
||
const event: Partial<SegmentEvent> = { | ||
userId: 'user-id-1', | ||
type: 'identify', | ||
traits: { | ||
first_time_buyer: true | ||
}, | ||
context: { | ||
personas: { | ||
computation_class: 'audience', | ||
computation_key: 'first_time_buyer', | ||
computation_id: 'aud_123' | ||
} | ||
} | ||
} | ||
|
||
mockTokenQueryResponse([{ advertiser: { id: 'advertiser-id-1' } }, { advertiser: { id: 'advertiser-id-2' } }]) | ||
mockDeleteProfilesMutation(deleteRequestBody) | ||
|
||
const responses = await testDestination.testAction('onDelete', { | ||
event, | ||
useDefaultMappings: true, | ||
mapping: { userId: 'user-id-1' }, | ||
settings: { apiKey: mockGqlKey } | ||
}) | ||
|
||
expect(responses[0].status).toBe(200) | ||
expectDeleteProfilesMutation(deleteRequestBody, ['user-id-1'], ['advertiser-id-1', 'advertiser-id-2']) | ||
}) | ||
}) |
73 changes: 73 additions & 0 deletions
73
...ages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/functions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { GQL_ENDPOINT, EXTERNAL_PROVIDER } from '../functions' | ||
|
||
export async function onDelete(request: any, payload: any[]) { | ||
return (async () => { | ||
const userId = payload[0].userId ?? payload[0].anonymousId | ||
const TokenQuery = `query TokenInfo { | ||
tokenInfo { | ||
scopesByAdvertiser { | ||
nodes { | ||
advertiser { | ||
id | ||
} | ||
} | ||
totalCount | ||
} | ||
} | ||
}` | ||
|
||
const res_token = await request(GQL_ENDPOINT, { | ||
body: JSON.stringify({ query: TokenQuery }), | ||
throwHttpErrors: false | ||
}) | ||
|
||
if (res_token.status !== 200) { | ||
throw new Error('Failed to fetch advertiser information: ' + res_token.statusText) | ||
} | ||
|
||
const result_token = await res_token.json() | ||
const advertiserNode = result_token.data?.tokenInfo?.scopesByAdvertiser?.nodes | ||
|
||
if (!advertiserNode || advertiserNode.length === 0) { | ||
throw new Error('No advertiser ID found.') | ||
} | ||
|
||
const advertiserIds = advertiserNode.map((node: { advertiser: { id: string } }) => node.advertiser.id) | ||
|
||
const formattedExternalIds = `["${userId}"]` | ||
const formattedAdvertiserIds = `[${advertiserIds.map((id: string) => `"${id}"`).join(', ')}]` | ||
|
||
const query = `mutation { | ||
deleteProfilesWithExternalIds( | ||
externalIds: ${formattedExternalIds}, | ||
advertiserIDs: ${formattedAdvertiserIds}, | ||
externalProvider: "${EXTERNAL_PROVIDER}" | ||
) { | ||
userErrors { | ||
message | ||
path | ||
} | ||
} | ||
}` | ||
|
||
const res = await request(GQL_ENDPOINT, { | ||
body: JSON.stringify({ query }), | ||
throwHttpErrors: false | ||
}) | ||
|
||
if (res.status !== 200) { | ||
throw new Error('Failed to delete profile: ' + res.statusText) | ||
} | ||
|
||
const result = await res.json() | ||
|
||
if (result.data.deleteProfilesWithExternalIds.userErrors.length > 0) { | ||
throw new Error( | ||
'Profile deletion was not successful: ' + | ||
result.data.deleteProfilesWithExternalIds.userErrors.map((e: any) => e.message).join(', ') | ||
) | ||
} | ||
|
||
return result | ||
})() | ||
} |
20 changes: 20 additions & 0 deletions
20
...estination-actions/src/destinations/stackadapt-audiences/deleteProfile/generated-types.ts
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
45 changes: 45 additions & 0 deletions
45
packages/destination-actions/src/destinations/stackadapt-audiences/deleteProfile/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import { ActionDefinition } from '@segment/actions-core' | ||
import { Settings } from '../generated-types' | ||
import { onDelete } from './functions' | ||
import { Payload } from './generated-types' | ||
|
||
const action: ActionDefinition<Settings, Payload> = { | ||
title: 'Delete Profiles', | ||
description: 'Deletes a profile by userId or anonymousId and advertiser IDs.', | ||
fields: { | ||
userId: { | ||
label: 'User ID', | ||
description: 'The user ID to delete.', | ||
type: 'string', | ||
required: false | ||
}, | ||
anonymousId: { | ||
label: 'Anonymous ID', | ||
description: 'The anonymous ID to delete.', | ||
type: 'string', | ||
required: false | ||
}, | ||
advertiserId: { | ||
label: 'Advertiser IDs', | ||
description: 'Comma-separated list of advertiser IDs. If not provided, it will query the token info.', | ||
type: 'string', | ||
required: false | ||
}, | ||
externalProvider: { | ||
label: 'External Provider', | ||
description: 'The external provider to delete the profile from.', | ||
type: 'string', | ||
required: false | ||
} | ||
}, | ||
perform: async (request, { payload }) => { | ||
// For single profile deletion | ||
return await onDelete(request, [payload]) | ||
}, | ||
performBatch: async (request, { payload }) => { | ||
// For batch profile deletion | ||
return await onDelete(request, payload) | ||
} | ||
} | ||
|
||
export default action |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters