Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
3 changes: 2 additions & 1 deletion packages/core/src/flags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
export const FLAGS = {
ACTIONS_GOOGLE_EC_AUDIENCE_MEMBERSHIP: 'actions-google-ec-audience-membership',
ACTIONS_BRAZE_COHORTS_AUDIENCE_MEMBERSHIP: 'actions-braze-cohorts-audience-membership',
ACTIONS_LINKEDIN_AUDIENCES_AUDIENCE_MEMBERSHIP: 'actions-linkedin-audiences-audience-membership'
ACTIONS_LINKEDIN_AUDIENCES_AUDIENCE_MEMBERSHIP: 'actions-linkedin-audiences-audience-membership',
ACTIONS_TIKTOK_AUDIENCES_AUDIENCE_MEMBERSHIP: 'actions-tiktok-audiences-audience-membership'
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,16 @@ export function getIDSchema(payload: GenericPayload): string[] {
return id_schema
}

const isHashedInformation = (information: string): boolean => new RegExp(/[0-9abcdef]{64}/gi).test(information)
export function normalizeEmail(email: string): string {
return email
.replace(/\+.*@/, '@')
.replace(/\.(?=.*@)/g, '')
.toLowerCase()
}

export const isHashedInformation = (information: string): boolean => new RegExp(/[0-9abcdef]{64}/gi).test(information)

const hash = (value: string): string => {
export const hash = (value: string): string => {
return processHashing(value, 'sha256', 'hex')
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import addToAudience from './addToAudience'

import removeFromAudience from './removeFromAudience'

import syncAudience from './syncAudience'

const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
name: 'TikTok Audiences',
slug: 'actions-tiktok-audiences',
Expand Down Expand Up @@ -160,6 +162,7 @@ const destination: AudienceDestinationDefinition<Settings, AudienceSettings> = {
}
},
actions: {
syncAudience,
addToAudience,
removeFromAudience,
addUser,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ export const enable_batching: InputField = {
unsafe_hidden: true
}

export const batch_keys: InputField = {
label: 'Batch Keys',
description: 'The keys to use for batching the events.',
type: 'string',
multiple: true,
default: ['send_email', 'send_phone', 'send_advertising_id', 'external_audience_id'],
unsafe_hidden: true
}

export const external_audience_id: InputField = {
label: 'External Audience ID',
description: "The Audience ID in TikTok's DB.",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import nock from 'nock'
import { createTestEvent, createTestIntegration } from '@segment/actions-core'
import Destination from '../../index'
import { BASE_URL } from '../../constants'
import { TIKTOK_AUDIENCES_API_VERSION } from '../../versioning-info'

const testDestination = createTestIntegration(Destination)

interface AuthTokens {
accessToken: string
refreshToken: string
}

const auth: AuthTokens = {
accessToken: 'test',
refreshToken: 'test'
}

const EXTERNAL_AUDIENCE_ID = '12345'
const ADVERTISER_ID = '123' // References audienceSettings.advertiserId
const ADVERTISING_ID = '4242' // References device.advertisingId
const ID_TYPE = 'EMAIL_SHA256' // References audienceSettings.idType

const event = createTestEvent({
event: 'Audience Entered',
type: 'track',
properties: {},
context: {
device: {
advertisingId: ADVERTISING_ID
},
traits: {
email: '[email protected]',
phone: '1234567890'
},
personas: {
audience_settings: {
advertiserId: ADVERTISER_ID,
idType: ID_TYPE
},
external_audience_id: EXTERNAL_AUDIENCE_ID
}
}
})

const updateUsersRequestBody = {
id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'],
advertiser_ids: [ADVERTISER_ID],
action: 'add',
batch_data: [
[
{
id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777',
audience_ids: [EXTERNAL_AUDIENCE_ID]
},
{
id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646',
audience_ids: [EXTERNAL_AUDIENCE_ID]
},
{
id: '0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6',
audience_ids: [EXTERNAL_AUDIENCE_ID]
}
]
]
}

describe('TiktokAudiences.addToAudience', () => {
it('should succeed if audience id is valid', async () => {
nock(`${BASE_URL}${TIKTOK_AUDIENCES_API_VERSION}`).post('/segment/mapping/', updateUsersRequestBody).reply(200)

const r = await testDestination.testAction('addToAudience', {
auth,
Comment on lines +68 to +73
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test file lives under syncAudience/__tests__ but the suite still targets addToAudience (describe name and testAction('addToAudience', ...)). As written it duplicates the existing addToAudience tests and does not exercise the new syncAudience action (including the audienceMembership add/delete fan-out behavior). Consider rewriting these tests to call testBatchAction('syncAudience', ...) (or unit-test syncAudience/functions.ts) and removing the duplicated addToAudience coverage here.

Copilot generated this review using guidance from repository custom instructions.
event,
settings: {},
useDefaultMappings: true,
Comment on lines +68 to +76
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new test file lives under syncAudience/__tests__, but the suite is named TiktokAudiences.addToAudience and all tests call testAction('addToAudience', ...), which means the new syncAudience action isn’t being tested at all (and these cases appear duplicated from tiktok-audiences/addToAudience/__tests__/index.test.ts). Update the tests to target syncAudience and cover both audienceMembership=true (add) and audienceMembership=false (delete), including mixed-membership performBatch behavior.

Copilot generated this review using guidance from repository custom instructions.
mapping: {
send_advertising_id: true
}
})

expect(r[0].status).toEqual(200)
expect(r[0].options.body).toMatchInlineSnapshot(
`"{\\"advertiser_ids\\":[\\"123\\"],\\"action\\":\\"add\\",\\"id_schema\\":[\\"EMAIL_SHA256\\",\\"PHONE_SHA256\\",\\"IDFA_SHA256\\"],\\"batch_data\\":[[{\\"id\\":\\"584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777\\",\\"audience_ids\\":[\\"12345\\"]},{\\"id\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\",\\"audience_ids\\":[\\"12345\\"]},{\\"id\\":\\"0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6\\",\\"audience_ids\\":[\\"12345\\"]}]]}"`
)
})

it('should normalize and hash emails correctly', async () => {
nock(`${BASE_URL}${TIKTOK_AUDIENCES_API_VERSION}`)
.post('/segment/mapping/', {
advertiser_ids: ['123'],
action: 'add',
id_schema: ['EMAIL_SHA256'],
batch_data: [
[
{
id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777',
audience_ids: [EXTERNAL_AUDIENCE_ID]
}
]
]
})
.reply(200)

const responses = await testDestination.testAction('addToAudience', {
event,
settings: {
advertiser_ids: ['123']
},
useDefaultMappings: true,
auth,
mapping: {
send_advertising_id: false,
send_phone: false
}
})

expect(responses[0].options.body).toMatchInlineSnapshot(
`"{\\"advertiser_ids\\":[\\"123\\"],\\"action\\":\\"add\\",\\"id_schema\\":[\\"EMAIL_SHA256\\"],\\"batch_data\\":[[{\\"id\\":\\"584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777\\",\\"audience_ids\\":[\\"12345\\"]}]]}"`
)
})

it('should normalize and hash phone correctly', async () => {
nock(`${BASE_URL}${TIKTOK_AUDIENCES_API_VERSION}`)
.post('/segment/mapping/', {
advertiser_ids: ['123'],
action: 'add',
id_schema: ['PHONE_SHA256'],
batch_data: [
[
{
id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646',
audience_ids: [EXTERNAL_AUDIENCE_ID]
}
]
]
})
.reply(200)

const responses = await testDestination.testAction('addToAudience', {
event,
settings: {
advertiser_ids: ['123']
},
useDefaultMappings: true,
auth,
mapping: {
send_advertising_id: false,
send_email: false
}
})

expect(responses[0].options.body).toMatchInlineSnapshot(
`"{\\"advertiser_ids\\":[\\"123\\"],\\"action\\":\\"add\\",\\"id_schema\\":[\\"PHONE_SHA256\\"],\\"batch_data\\":[[{\\"id\\":\\"c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646\\",\\"audience_ids\\":[\\"12345\\"]}]]}"`
)
})

it('should fail if an audience id is invalid', async () => {
const anotherEvent = createTestEvent({
event: 'Audience Entered',
type: 'track',
properties: {
audience_key: 'personas_test_audience'
},
context: {
device: {
advertisingId: ADVERTISING_ID
},
traits: {
email: '[email protected]',
phone: '1234567890'
},
personas: {
audience_settings: {
advertiserId: ADVERTISER_ID,
idType: ID_TYPE
},
external_audience_id: 'THIS_ISNT_REAL'
}
}
})

nock(`${BASE_URL}${TIKTOK_AUDIENCES_API_VERSION}/segment/mapping/`)
.post(/.*/, {
id_schema: ['EMAIL_SHA256', 'PHONE_SHA256', 'IDFA_SHA256'],
advertiser_ids: [ADVERTISER_ID],
action: 'add',
batch_data: [
[
{
id: '584c4423c421df49955759498a71495aba49b8780eb9387dff333b6f0982c777',
audience_ids: ['THIS_ISNT_REAL']
},
{
id: 'c775e7b757ede630cd0aa1113bd102661ab38829ca52a6422ab782862f268646',
audience_ids: ['THIS_ISNT_REAL']
},
{
id: '0315b4020af3eccab7706679580ac87a710d82970733b8719e70af9b57e7b9e6',
audience_ids: ['THIS_ISNT_REAL']
}
]
]
})
.reply(400)

const r = await testDestination.testAction('addToAudience', {
event: anotherEvent,
settings: {
advertiser_ids: ['123']
},
useDefaultMappings: true,
auth,
mapping: {}
})

expect(r[0].status).toEqual(400)
})

it('should fail if all the send fields are false', async () => {
nock(`${BASE_URL}${TIKTOK_AUDIENCES_API_VERSION}/segment/mapping/`).post(/.*/, updateUsersRequestBody).reply(200)

await expect(
testDestination.testAction('addToAudience', {
event,
settings: {
advertiser_ids: ['123']
},
useDefaultMappings: true,
auth,
mapping: {
selected_advertiser_id: '123',
audience_id: '123456',
send_email: false,
send_advertising_id: false,
send_phone: false
}
})
).rejects.toThrow('At least one of `Send Email`, `Send Phone` or `Send Advertising ID` must be set to `true`.')
})
it('should fail if email and/or advertising_id is not in the payload', async () => {
nock(`${BASE_URL}${TIKTOK_AUDIENCES_API_VERSION}/segment/mapping/`).post(/.*/, updateUsersRequestBody).reply(400)

delete event?.context?.device
delete event?.context?.traits

await expect(
testDestination.testAction('addToAudience', {
event,
settings: {
advertiser_ids: ['123']
},
useDefaultMappings: true,
auth,
mapping: {
send_email: true,
send_advertising_id: true
}
})
).rejects.toThrowError('At least one of Email Id or Advertising ID must be provided.')
})
})
Loading
Loading