From b3abb991f12f25d312b561ed6063416161b1aed6 Mon Sep 17 00:00:00 2001 From: Joe Ayoub Date: Fri, 3 Oct 2025 15:01:51 +0100 Subject: [PATCH] Started work on snap audience rewrite --- .../destinations/snap-audiences/constants.ts | 3 + .../snap-audiences/generated-types.ts | 2 +- .../src/destinations/snap-audiences/index.ts | 102 ++++------- .../syncAudience/__tests__/index.test.ts | 2 +- .../snap-audiences/syncAudience/constants.ts | 5 + .../snap-audiences/syncAudience/fields.ts | 85 +++++++++ .../snap-audiences/syncAudience/functions.ts | 109 ++++++++++++ .../syncAudience/generated-types.ts | 18 +- .../snap-audiences/syncAudience/index.ts | 166 +----------------- .../snap-audiences/syncAudience/types.ts | 17 ++ .../snap-audiences/syncAudience/utils.ts | 90 ---------- .../src/destinations/snap-audiences/types.ts | 21 +++ 12 files changed, 297 insertions(+), 323 deletions(-) create mode 100644 packages/destination-actions/src/destinations/snap-audiences/constants.ts create mode 100644 packages/destination-actions/src/destinations/snap-audiences/syncAudience/constants.ts create mode 100644 packages/destination-actions/src/destinations/snap-audiences/syncAudience/fields.ts create mode 100644 packages/destination-actions/src/destinations/snap-audiences/syncAudience/functions.ts create mode 100644 packages/destination-actions/src/destinations/snap-audiences/syncAudience/types.ts delete mode 100644 packages/destination-actions/src/destinations/snap-audiences/syncAudience/utils.ts create mode 100644 packages/destination-actions/src/destinations/snap-audiences/types.ts diff --git a/packages/destination-actions/src/destinations/snap-audiences/constants.ts b/packages/destination-actions/src/destinations/snap-audiences/constants.ts new file mode 100644 index 00000000000..2031c8aa811 --- /dev/null +++ b/packages/destination-actions/src/destinations/snap-audiences/constants.ts @@ -0,0 +1,3 @@ +export const ACCESS_TOKEN_URL = 'https://accounts.snapchat.com/login/oauth2/access_token' + +export const DEFAULT_RETENTION_DAYS = 9999 \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/snap-audiences/generated-types.ts b/packages/destination-actions/src/destinations/snap-audiences/generated-types.ts index c2f8e17464e..fe839991952 100644 --- a/packages/destination-actions/src/destinations/snap-audiences/generated-types.ts +++ b/packages/destination-actions/src/destinations/snap-audiences/generated-types.ts @@ -18,7 +18,7 @@ export interface AudienceSettings { */ description?: string /** - * # of days to retain audience members. (Default retention is lifetime represented as 9999) + * Number of days to retain audience members. (Default retention is lifetime represented as 9999) */ retention_in_days?: number } diff --git a/packages/destination-actions/src/destinations/snap-audiences/index.ts b/packages/destination-actions/src/destinations/snap-audiences/index.ts index 357a0881638..7fbaf3456c8 100644 --- a/packages/destination-actions/src/destinations/snap-audiences/index.ts +++ b/packages/destination-actions/src/destinations/snap-audiences/index.ts @@ -1,27 +1,8 @@ import { AudienceDestinationDefinition, defaultValues, IntegrationError } from '@segment/actions-core' import type { Settings, AudienceSettings } from './generated-types' import syncAudience from './syncAudience' -const ACCESS_TOKEN_URL = 'https://accounts.snapchat.com/login/oauth2/access_token' - -interface RefreshTokenResponse { - access_token: string -} -interface SnapAudienceResponse { - segments: { - segment: { - id: string - } - }[] -} -interface CreateAudienceReq { - segments: { - name: string - source_type: string - ad_account_id: string - description: string - retention_in_days: number - }[] -} +import { CreateAudienceReq, RefreshTokenResponse, SnapAudienceResponse } from './types' +import { ACCESS_TOKEN_URL, DEFAULT_RETENTION_DAYS } from './constants' const destination: AudienceDestinationDefinition = { name: 'Snap Audiences (Actions)', @@ -93,24 +74,10 @@ const destination: AudienceDestinationDefinition = { eventSlug: 'journeys_step_entered_track' }, { - name: 'Sync Audience with Email', - subscribe: 'type = "track" and context.traits.email exists', + name: 'Sync Engage Audience', + subscribe: 'type = "track" or type = "identify"', partnerAction: 'syncAudience', - mapping: { ...defaultValues(syncAudience.fields), schema_type: 'EMAIL_SHA256' }, - type: 'automatic' - }, - { - name: 'Sync Audience with Phone', - subscribe: 'type = "track" and properties.phone exists', - partnerAction: 'syncAudience', - mapping: { ...defaultValues(syncAudience.fields), schema_type: 'PHONE_SHA256' }, - type: 'automatic' - }, - { - name: 'Sync Audience with Mobile AD ID', - subscribe: 'type = "track" and context.device.advertisingId exists', - partnerAction: 'syncAudience', - mapping: { ...defaultValues(syncAudience.fields), schema_type: 'MOBILE_AD_ID_SHA256' }, + mapping: { ...defaultValues(syncAudience.fields)}, type: 'automatic' } ], @@ -118,8 +85,7 @@ const destination: AudienceDestinationDefinition = { customAudienceName: { type: 'string', label: 'Audience Name', - description: - 'Name for the audience that will be created in Snap. Defaults to the snake_cased Segment audience name if left blank.', + description: 'Name for the audience that will be created in Snap. Defaults to the snake_cased Segment audience name if left blank.', default: '', required: false }, @@ -133,7 +99,7 @@ const destination: AudienceDestinationDefinition = { retention_in_days: { type: 'number', label: 'Retention in days', - description: '# of days to retain audience members. (Default retention is lifetime represented as 9999)', + description: 'Number of days to retain audience members. (Default retention is lifetime represented as 9999)', default: 9999, required: false } @@ -144,44 +110,50 @@ const destination: AudienceDestinationDefinition = { full_audience_sync: false }, async createAudience(request, createAudienceInput) { - const audienceName = createAudienceInput.audienceName - const ad_account_id = createAudienceInput.settings.ad_account_id - const { customAudienceName, description, retention_in_days } = createAudienceInput.audienceSettings || {} + const { + audienceName, + settings: { ad_account_id } = {}, + audienceSettings: { + customAudienceName, + description, + retention_in_days + } = {} + } = createAudienceInput if (!audienceName) { throw new IntegrationError('Missing audience name value', 'MISSING_REQUIRED_FIELD', 400) } - const response = await request(`https://adsapi.snapchat.com/v1/adaccounts/${ad_account_id}/segments`, { + if(!ad_account_id){ + throw new IntegrationError('Missing Ad Account ID. Please configure the Ad Account ID in the destination settings.', 'MISSING_REQUIRED_FIELD', 400) + } + + const json: CreateAudienceReq = { + segments: [ + { + name: customAudienceName || audienceName, + source_type: 'FIRST_PARTY', + ad_account_id, + description: description || `Audience ${audienceName} created by Segment`, + retention_in_days: retention_in_days || DEFAULT_RETENTION_DAYS + } + ] + } + + const response = await request(`https://adsapi.snapchat.com/v1/adaccounts/${ad_account_id}/segments`, { method: 'POST', - json: { - segments: [ - { - name: customAudienceName !== '' ? customAudienceName : audienceName, - source_type: 'FIRST_PARTY', - ad_account_id, - description, - retention_in_days - } - ] - } as CreateAudienceReq + json }) - const data: SnapAudienceResponse = await response.json() - const snapAudienceId = data.segments[0].segment.id - - return { externalId: snapAudienceId } + return { externalId: response.data.segments[0].segment.id } }, getAudience: async (request, { externalId }) => { - const response = await request(`https://adsapi.snapchat.com/v1/segments/${externalId}`, { + const response = await request(`https://adsapi.snapchat.com/v1/segments/${externalId}`, { method: 'GET' }) - const data: SnapAudienceResponse = await response.json() - const snapAudienceId = data.segments[0].segment.id - - return { externalId: snapAudienceId } + return { externalId: response.data.segments[0].segment.id } } } } diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/__tests__/index.test.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/__tests__/index.test.ts index 594b223dc8d..76a17b93afe 100644 --- a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/__tests__/index.test.ts +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/__tests__/index.test.ts @@ -2,7 +2,7 @@ import nock from 'nock' import { createTestEvent, createTestIntegration, PayloadValidationError } from '@segment/actions-core' import Destination from '../../index' import { processHashing } from '../../../../lib/hashing-utils' -import { normalize, normalizePhone } from '../utils' +import { normalize, normalizePhone } from '../functions' const testDestination = createTestIntegration(Destination) diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/constants.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/constants.ts new file mode 100644 index 00000000000..89371a61b20 --- /dev/null +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/constants.ts @@ -0,0 +1,5 @@ +export const SCHEMA_TYPES = { + EMAIL: 'EMAIL_SHA256', + PHONE: 'PHONE_SHA256', + MAID: 'MOBILE_AD_ID_SHA256' +} as const \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/fields.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/fields.ts new file mode 100644 index 00000000000..87e5daf4edb --- /dev/null +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/fields.ts @@ -0,0 +1,85 @@ +import { InputField } from '@segment/actions-core' + +export const fields: Record = { + external_audience_id: { + type: 'string', + label: 'External Audience ID', + description: 'Unique Audience Identifier returned by the createAudience() function call.', + required: true, + unsafe_hidden: true, + default: { + '@path': '$.context.personas.external_audience_id' + } + }, + audienceKey: { + type: 'string', + label: 'Audience Key', + description: 'Audience key.', + required: true, + unsafe_hidden: true, + default: { + '@path': '$.context.personas.computation_key' + } + }, + props: { + label: 'Properties object', + description: 'Object that contains audience name and value.', + type: 'object', + required: true, + unsafe_hidden: true, + default: { '@path': '$.properties' } + }, + phone: { + label: 'Phone', + description: "If using phone as the identifier an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'phone' is configured as an additional identifier in the Audience settings tab.", + type: 'string', + required: false, + default: { '@path': '$.properties.phone' }, + }, + email: { + label: 'Email', + description: "The user's email address.", + type: 'string', + required: false, + default: { '@path': '$.properties.email' }, + }, + advertising_id: { + label: 'Mobile Advertising ID', + description: + "If using Mobile Ad ID as the identifier an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'ios.idfa' is configured to 'ios_idfa' and 'android.idfa' is configured to 'android_idfa' in the Audience settings tab.", + type: 'string', + required: false, + default: { + '@if': { + exists: { '@path': '$.properties.android_idfa' }, + then: { '@path': '$.properties.android_idfa' }, + else: { '@path': '$.properties.ios_idfa' } + } + } + }, + enable_batching: { + label: 'Enable Batching', + description: 'When enabled, events will be batched before being sent to Snap.', + type: 'boolean', + required: true, + default: true + }, + max_batch_size: { + label: 'Max Batch Size', + description: 'Maximum number of API calls to include in a batch. Defaults to 100,000 which is the maximum allowed by Snap.', + type: 'number', + required: true, + minimum: 1, + maximum: 100_000, + default: 100_000 + }, + batch_keys: { + label: 'Batch Keys', + description: 'The keys to use for batching the events. Ensures events from different audiences are sent in separate batches. This is Segment default behavior with Engage Audiences anyway.', + type: 'string', + unsafe_hidden: true, + required: false, + multiple: true, + default: ['external_audience_id'] + } +} \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/functions.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/functions.ts new file mode 100644 index 00000000000..fa4c0199a1b --- /dev/null +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/functions.ts @@ -0,0 +1,109 @@ +import type { Payload } from './generated-types' +import { processHashing } from '../../../lib/hashing-utils' +import { RequestClient, MultiStatusResponse } from '@segment/actions-core' +import { PayloadWithIndex, AddRemoveUsersJSON, SchemaType } from './types' +import { SCHEMA_TYPES } from './constants' + + +export async function send(request: RequestClient, payload: Payload[]) { + const payloads: PayloadWithIndex[] = payload.map((p, index) => ({ ...p, index })) + const { external_audience_id } = payload[0] + const multiStatusResponse = new MultiStatusResponse() + + const grouped = payloads.reduce( + (acc, p) => { + const hasValue = Boolean(p.email || p.phone || p.advertising_id) + if (!hasValue) { + multiStatusResponse.setErrorResponseAtIndex(p.index, { + status: 400, + errortype: 'PAYLOAD_VALIDATION_FAILED', + errormessage: 'One of "email" or "phone" or "Mobile Advertising ID" is required.' + }); + return acc + } + + const isAdd = Boolean(p.props[p.audienceKey]) + + if (p.email) (isAdd ? acc.addEmail : acc.removeEmail).push(p) + if (p.phone) (isAdd ? acc.addPhone : acc.removePhone).push(p) + if (p.advertising_id) (isAdd ? acc.addMAID : acc.removeMAID).push(p) + + return acc + }, + { + addEmail: [] as PayloadWithIndex[], + addPhone: [] as PayloadWithIndex[], + addMAID: [] as PayloadWithIndex[], + removeEmail: [] as PayloadWithIndex[], + removePhone: [] as PayloadWithIndex[], + removeMAID: [] as PayloadWithIndex[], + } + ) + + const url = `https://adsapi.snapchat.com/v1/segments/${external_audience_id}/users` + + return await Promise.all([ + sendRequest(request, grouped.addEmail, SCHEMA_TYPES.EMAIL, "POST", url), + sendRequest(request, grouped.addPhone, SCHEMA_TYPES.PHONE, "POST", url), + sendRequest(request, grouped.addMAID, SCHEMA_TYPES.MAID, "POST", url), + sendRequest(request, grouped.removeEmail, SCHEMA_TYPES.EMAIL, "DELETE", url), + sendRequest(request, grouped.removePhone, SCHEMA_TYPES.PHONE, "DELETE", url), + sendRequest(request, grouped.removeMAID, SCHEMA_TYPES.MAID, "DELETE", url) + ]) + +} + +async function sendRequest(request: RequestClient, payloads: PayloadWithIndex[], type: SchemaType, method: "POST" | "DELETE", url: string) { + if (payloads.length === 0) return { skipped: true } + + const json = buildJSON(payloads, type) + return await request(url, { method, json }) +} + +function buildJSON(payloads: PayloadWithIndex[], type: SchemaType): AddRemoveUsersJSON { + const data: [string][] = payloads.reduce<[string][]>((acc, p) => { + let value: string | undefined; + if (type === SCHEMA_TYPES.EMAIL && p.email) value = processHashing(p.email, 'sha256', 'hex', normalize); + else if (type === SCHEMA_TYPES.PHONE && p.phone) value = processHashing(p.phone, 'sha256', 'hex', normalizePhone); + else if (type === SCHEMA_TYPES.MAID && p.advertising_id) value = processHashing(p.advertising_id, 'sha256', 'hex', normalize); + + if (value) acc.push([value]) + return acc + }, []) + + return { + users: [ + { + schema: [type], + data + } + ] + } +} + +export const normalize = (identifier: string): string => { + return identifier.trim().toLowerCase() +} + +/* + Normalize phone numbers by + - removing any double 0 in front of the country code + - if the number itself begins with a 0 this should be removed + - Also exclude any non-numeric characters such as whitespace, parentheses, '+', or '-'. +*/ +export const normalizePhone = (phone: string): string => { + // Remove non-numeric characters and parentheses, '+', '-', ' ' + let normalizedPhone = phone.replace(/[\s()+-]/g, '') + + // Remove leading "00" if present + if (normalizedPhone.startsWith('00')) { + normalizedPhone = normalizedPhone.substring(2) + } + + // Remove leading zero if present (for local numbers) + if (normalizedPhone.startsWith('0')) { + normalizedPhone = normalizedPhone.substring(1) + } + + return normalizedPhone +} diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/generated-types.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/generated-types.ts index a28ea247f5f..d0294ccf5ff 100644 --- a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/generated-types.ts +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/generated-types.ts @@ -10,17 +10,13 @@ export interface Payload { */ audienceKey: string /** - * A computed object for track events. + * Object that contains audience name and value. */ props: { [k: string]: unknown } /** - * Choose the type of identifier to use when adding users to Snap. - */ - schema_type: string - /** - * If using phone as the identifier, an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'phone' is configured as an additional identifier in the Audience settings tab. + * If using phone as the identifier an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'phone' is configured as an additional identifier in the Audience settings tab. */ phone?: string /** @@ -28,11 +24,19 @@ export interface Payload { */ email?: string /** - * If using Mobile Ad ID as the identifier, an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'ios.idfa' and 'android.idfa' are configured as an additional identifier in the Audience settings tab. + * If using Mobile Ad ID as the identifier an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'ios.idfa' is configured to 'ios_idfa' and 'android.idfa' is configured to 'android_idfa' in the Audience settings tab. */ advertising_id?: string /** * When enabled, events will be batched before being sent to Snap. */ enable_batching: boolean + /** + * Maximum number of API calls to include in a batch. + */ + max_batch_size: number + /** + * The keys to use for batching the events. Ensures events from different audiences are sent in separate batches. This is Segment default behavior with Engage Audiences anyway. + */ + batch_keys?: string[] } diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/index.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/index.ts index 057a7117f23..d7e78dea1dc 100644 --- a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/index.ts +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/index.ts @@ -1,172 +1,20 @@ -import type { ActionDefinition, RequestClient } from '@segment/actions-core' -import { PayloadValidationError } from '@segment/actions-core' +import type { ActionDefinition } from '@segment/actions-core' import type { Settings } from '../generated-types' import type { Payload } from './generated-types' -import { validationError, sortPayload } from './utils' +import { fields } from './fields' +import { send } from './functions' const action: ActionDefinition = { title: 'Sync Audience', - description: 'Sync users to Snap', + description: 'Sync Engage Audiences to Snap', defaultSubscription: 'type = "track"', - fields: { - external_audience_id: { - type: 'string', - label: 'External Audience ID', - description: 'Unique Audience Identifier returned by the createAudience() function call.', - required: true, - unsafe_hidden: true, - default: { - '@path': '$.context.personas.external_audience_id' - } - }, - audienceKey: { - type: 'string', - label: 'Audience Key', - description: 'Audience key.', - required: true, - unsafe_hidden: true, - default: { - '@path': '$.context.personas.computation_key' - } - }, - props: { - label: 'Properties object', - description: 'A computed object for track events.', - type: 'object', - required: true, - unsafe_hidden: true, - default: { '@path': '$.properties' } - }, - schema_type: { - type: 'string', - choices: [ - { value: 'MOBILE_AD_ID_SHA256', label: 'Mobile ID' }, - { value: 'PHONE_SHA256', label: 'Phone' }, - { value: 'EMAIL_SHA256', label: 'Email' } - ], - label: 'External ID Type', - required: true, - description: 'Choose the type of identifier to use when adding users to Snap.', - default: 'EMAIL_SHA256', - disabledInputMethods: ['literal', 'variable', 'function', 'freeform', 'enrichment'] - }, - phone: { - label: 'Phone', - description: - "If using phone as the identifier, an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'phone' is configured as an additional identifier in the Audience settings tab.", - type: 'string', - required: false, - default: { '@path': '$.properties.phone' }, - depends_on: { - match: 'all', - conditions: [ - { - fieldKey: 'schema_type', - operator: 'is', - value: 'PHONE_SHA256' - } - ] - }, - category: 'hashedPII' - }, - email: { - label: 'Email', - description: "The user's email address.", - type: 'string', - required: false, - default: { '@path': '$.context.traits.email' }, - depends_on: { - match: 'all', - conditions: [ - { - fieldKey: 'schema_type', - operator: 'is', - value: 'EMAIL_SHA256' - } - ] - }, - category: 'hashedPII' - }, - advertising_id: { - label: 'Mobile Advertising ID', - description: - "If using Mobile Ad ID as the identifier, an additional setup step is required when connecting the Destination to the Audience. Please ensure that 'ios.idfa' and 'android.idfa' are configured as an additional identifier in the Audience settings tab.", - type: 'string', - required: false, - default: { - '@path': '$.context.device.advertisingId' - }, - depends_on: { - match: 'all', - conditions: [ - { - fieldKey: 'schema_type', - operator: 'is', - value: 'MOBILE_AD_ID_SHA256' - } - ] - }, - category: 'hashedPII' - }, - enable_batching: { - label: 'Enable Batching', - description: 'When enabled, events will be batched before being sent to Snap.', - type: 'boolean', - required: true, - default: true - } - }, + fields, perform: async (request, { payload }) => { - return processPayload(request, [payload]) + return send(request, [payload]) }, performBatch: async (request, { payload }) => { - return processPayload(request, payload) + return send(request, payload) } } export default action - -const processPayload = async (request: RequestClient, payload: Payload[]) => { - const { external_audience_id, schema_type } = payload[0] - const { enteredAudience, exitedAudience } = sortPayload(payload) - - if (enteredAudience.length === 0 && exitedAudience.length === 0) { - throw new PayloadValidationError(`No ${validationError(schema_type)} identifier present in payload(s)`) - } - - const requests = [] - - if (enteredAudience.length > 0) { - requests.push( - request(`https://adsapi.snapchat.com/v1/segments/${external_audience_id}/users`, { - method: 'post', - json: { - users: [ - { - schema: [`${schema_type}`], - data: enteredAudience - } - ] - } - }) - ) - } - - if (exitedAudience.length > 0) { - requests.push( - request(`https://adsapi.snapchat.com/v1/segments/${external_audience_id}/users`, { - method: 'delete', - json: { - users: [ - { - schema: [`${schema_type}`], - data: exitedAudience - } - ] - } - }) - ) - } - - return await Promise.all(requests) -} diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/types.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/types.ts new file mode 100644 index 00000000000..21bbdbe50ef --- /dev/null +++ b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/types.ts @@ -0,0 +1,17 @@ +import { Payload } from './generated-types' +import { SCHEMA_TYPES } from './constants' + +export interface PayloadWithIndex extends Payload { + index: number +} + +export interface AddRemoveUsersJSON { + users: [ + { + schema: ["EMAIL_SHA256"] | ["PHONE_SHA256"] | ["MOBILE_AD_ID_SHA256"], + data: [string][] + } + ] +} + +export type SchemaType = typeof SCHEMA_TYPES[keyof typeof SCHEMA_TYPES]; \ No newline at end of file diff --git a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/utils.ts b/packages/destination-actions/src/destinations/snap-audiences/syncAudience/utils.ts deleted file mode 100644 index 18ae9fb8fee..00000000000 --- a/packages/destination-actions/src/destinations/snap-audiences/syncAudience/utils.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Payload } from './generated-types' -import { processHashing } from '../../../lib/hashing-utils' - -// Filters out events with missing identifiers and sorts based on audience entered/exited -export const sortPayload = (payload: Payload[]) => { - return payload.reduce<{ - enteredAudience: string[][] - exitedAudience: string[][] - }>( - (acc, payloadItem) => { - const audienceEntered = payloadItem.props[payloadItem.audienceKey] - const externalId = validateAndExtractIdentifier( - payloadItem.schema_type, - payloadItem.email, - payloadItem.phone, - payloadItem.advertising_id - ) - - if (externalId) { - if (audienceEntered) { - acc.enteredAudience.push([externalId]) - } else { - acc.exitedAudience.push([externalId]) - } - } - - return acc - }, - { enteredAudience: [], exitedAudience: [] } - ) -} -export const validationError = (schema_type: string): string => { - switch (schema_type) { - case 'MOBILE_AD_ID_SHA256': - return 'Mobile Advertising ID' - case 'EMAIL_SHA256': - return 'Email' - case 'PHONE_SHA256': - return 'Phone number' - default: - return 'Identifier' - } -} - -// Returns normalized and hashed identifier or null if not present -const validateAndExtractIdentifier = ( - schemaType: string, - email: string | undefined, - phone: string | undefined, - mobileAdId: string | undefined -): string | null => { - if (schemaType === 'EMAIL_SHA256' && email) { - return processHashing(email, 'sha256', 'hex', normalize) - } - if (schemaType === 'MOBILE_AD_ID_SHA256' && mobileAdId) { - return processHashing(mobileAdId, 'sha256', 'hex', normalize) - } - if (schemaType === 'PHONE_SHA256' && phone) { - return processHashing(phone, 'sha256', 'hex', normalizePhone) - } - - return null -} - -export const normalize = (identifier: string): string => { - return identifier.trim().toLowerCase() -} - -/* - Normalize phone numbers by - - removing any double 0 in front of the country code - - if the number itself begins with a 0 this should be removed - - Also exclude any non-numeric characters such as whitespace, parentheses, '+', or '-'. -*/ -export const normalizePhone = (phone: string): string => { - // Remove non-numeric characters and parentheses, '+', '-', ' ' - let normalizedPhone = phone.replace(/[\s()+-]/g, '') - - // Remove leading "00" if present - if (normalizedPhone.startsWith('00')) { - normalizedPhone = normalizedPhone.substring(2) - } - - // Remove leading zero if present (for local numbers) - if (normalizedPhone.startsWith('0')) { - normalizedPhone = normalizedPhone.substring(1) - } - - return normalizedPhone -} diff --git a/packages/destination-actions/src/destinations/snap-audiences/types.ts b/packages/destination-actions/src/destinations/snap-audiences/types.ts new file mode 100644 index 00000000000..18ed81637d7 --- /dev/null +++ b/packages/destination-actions/src/destinations/snap-audiences/types.ts @@ -0,0 +1,21 @@ +export interface RefreshTokenResponse { + access_token: string +} + +export interface SnapAudienceResponse { + segments: { + segment: { + id: string + } + }[] +} + +export interface CreateAudienceReq { + segments: { + name: string + source_type: string + ad_account_id: string + description: string + retention_in_days: number + }[] +} \ No newline at end of file