Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c303690
getting started on audience membership
joe-ayoub-segment Mar 10, 2026
7992933
adding retlMembership
joe-ayoub-segment Mar 10, 2026
0a65bae
updating payload objects with audience_membership
joe-ayoub-segment Mar 10, 2026
3e7cc58
moving logic
joe-ayoub-segment Mar 10, 2026
c2f52f6
moving membership out of payload
joe-ayoub-segment Mar 11, 2026
127121f
update
joe-ayoub-segment Mar 11, 2026
e5ce255
updates
joe-ayoub-segment Mar 11, 2026
6e308a1
update
joe-ayoub-segment Mar 11, 2026
3141486
migrated amplitude cohorts
joe-ayoub-segment Mar 11, 2026
42f5987
type fix
joe-ayoub-segment Mar 11, 2026
b0add83
updating tests
joe-ayoub-segment Mar 12, 2026
a7d2a8b
adding flag
joe-ayoub-segment Mar 12, 2026
ab36b5a
fixing tests for amplitude cohorts
joe-ayoub-segment Mar 12, 2026
25a89cd
fixing test authentication
joe-ayoub-segment Mar 12, 2026
acf98ef
fixing amplitude cohort test
joe-ayoub-segment Mar 12, 2026
6f1f38c
adding flags to core
joe-ayoub-segment Mar 13, 2026
29f4186
adding google enhanced conversions
joe-ayoub-segment Mar 13, 2026
8c533df
adding facebook custom audiences
joe-ayoub-segment Mar 13, 2026
f86ee88
errors and flags update
joe-ayoub-segment Mar 13, 2026
0242bc7
braze cohorts audienceMembership migration
joe-ayoub-segment Mar 14, 2026
a89a42b
braze cohorts core complete apart from tests
joe-ayoub-segment Mar 14, 2026
616d193
adding tests for flags = true
joe-ayoub-segment Mar 14, 2026
ca04e80
removing snapshot
joe-ayoub-segment Mar 14, 2026
87a57e0
adding snapshot
joe-ayoub-segment Mar 14, 2026
ec0a6be
upating snapshot
joe-ayoub-segment Mar 14, 2026
2c76e47
linkedin audiences using audienceMembership from Core
joe-ayoub-segment Mar 14, 2026
b82ddbe
removing core flag
joe-ayoub-segment Mar 17, 2026
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
242 changes: 242 additions & 0 deletions packages/core/src/__tests__/audience-membership.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { createTestIntegration } from '../create-test-integration'
import { resolveAudienceMembership } from '../audience-membership'
import { DestinationDefinition } from '../destination-kit'
import { ExecuteInput } from '../destination-kit/types'
import { JSONObject } from '../json-object'

// --- Unit tests for the helper ---

describe('resolveAudienceMembership', () => {
it('returns undefined when rawData is undefined', () => {
expect(resolveAudienceMembership(undefined)).toBeUndefined()
})

it('returns undefined when computation_class is not audience', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'computed_trait', computation_key: 'my_audience' } },
properties: { my_audience: true }
})
).toBeUndefined()
})

it('returns undefined when computation_class is missing', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_key: 'my_audience' } },
properties: { my_audience: true }
})
).toBeUndefined()
})

it('returns undefined when computation_key is missing', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'audience' } },
properties: { my_audience: true }
})
).toBeUndefined()
})

it('returns undefined when the membership value is not a boolean', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'audience', computation_key: 'my_audience' } },
properties: { my_audience: 'true' }
})
).toBeUndefined()
})

it('returns undefined when the computation_key is not present in properties', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'audience', computation_key: 'my_audience' } },
properties: {}
})
).toBeUndefined()
})

it('returns true when the user is being added to the audience', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'audience', computation_key: 'my_audience' } },
properties: { my_audience: true }
})
).toBe(true)
})

it('returns false when the user is being removed from the audience', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'audience', computation_key: 'my_audience' } },
properties: { my_audience: false }
})
).toBe(false)
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

These unit tests call resolveAudienceMembership without an event type, and expect membership to be inferred from properties. The current implementation only resolves Engage membership when type is identify (from traits) or track (from properties), so these assertions will fail. Update the test inputs to include type and put the boolean on traits for identify (or switch to track + properties).

Copilot uses AI. Check for mistakes.
})

it('returns undefined when context is missing', () => {
expect(
resolveAudienceMembership({
properties: { my_audience: true }
})
).toBeUndefined()
})

it('returns undefined when properties is missing', () => {
expect(
resolveAudienceMembership({
context: { personas: { computation_class: 'audience', computation_key: 'my_audience' } }
})
).toBeUndefined()
})
})

// --- Integration tests verifying audienceMembership is set on ExecuteInput ---

describe('audienceMembership on ExecuteInput in perform()', () => {
it('is true when user is being added to an audience', async () => {
let capturedData: ExecuteInput<JSONObject, JSONObject> | undefined

const destination: DestinationDefinition<JSONObject> = {
name: 'Test Destination',
mode: 'cloud',
authentication: { scheme: 'custom', fields: {} },
actions: {
testAction: {
title: 'Test Action',
description: 'Test',
fields: {
userId: { label: 'User ID', description: 'The user ID', type: 'string' }
},
perform: (_request, data) => {
capturedData = data as ExecuteInput<JSONObject, JSONObject>
}
}
}
}

const testDestination = createTestIntegration(destination)
await testDestination.testAction('testAction', {
mapping: { userId: { '@path': '$.userId' } },
event: {
type: 'identify',
userId: 'user-1',
context: {
personas: { computation_class: 'audience', computation_key: 'my_audience' }
},
properties: { my_audience: true }
}
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

These integration tests use an identify event with the audience membership boolean in properties, but engageAudienceMembership reads traits[computation_key] for identify events. As written, capturedData.audienceMembership will be undefined and the expectations will fail. Put the boolean on traits for identify (or change the event type to track if you want to keep properties).

Copilot uses AI. Check for mistakes.
})

expect(capturedData?.audienceMembership).toBe(true)
})

it('is false when user is being removed from an audience', async () => {
let capturedData: ExecuteInput<JSONObject, JSONObject> | undefined

const destination: DestinationDefinition<JSONObject> = {
name: 'Test Destination',
mode: 'cloud',
authentication: { scheme: 'custom', fields: {} },
actions: {
testAction: {
title: 'Test Action',
description: 'Test',
fields: {
userId: { label: 'User ID', description: 'The user ID', type: 'string' }
},
perform: (_request, data) => {
capturedData = data as ExecuteInput<JSONObject, JSONObject>
}
}
}
}

const testDestination = createTestIntegration(destination)
await testDestination.testAction('testAction', {
mapping: { userId: { '@path': '$.userId' } },
event: {
type: 'identify',
userId: 'user-1',
context: {
personas: { computation_class: 'audience', computation_key: 'my_audience' }
},
properties: { my_audience: false }
}
})

expect(capturedData?.audienceMembership).toBe(false)
})

it('is undefined for non-audience events', async () => {
let capturedData: ExecuteInput<JSONObject, JSONObject> | undefined

const destination: DestinationDefinition<JSONObject> = {
name: 'Test Destination',
mode: 'cloud',
authentication: { scheme: 'custom', fields: {} },
actions: {
testAction: {
title: 'Test Action',
description: 'Test',
fields: {
userId: { label: 'User ID', description: 'The user ID', type: 'string' }
},
perform: (_request, data) => {
capturedData = data as ExecuteInput<JSONObject, JSONObject>
}
}
}
}

const testDestination = createTestIntegration(destination)
await testDestination.testAction('testAction', {
mapping: { userId: { '@path': '$.userId' } },
event: {
type: 'track',
userId: 'user-1',
properties: { foo: 'bar' }
}
})

expect(capturedData?.audienceMembership).toBeUndefined()
})

it('does not modify the payload object', async () => {
let capturedData: ExecuteInput<JSONObject, JSONObject> | undefined

const destination: DestinationDefinition<JSONObject> = {
name: 'Test Destination',
mode: 'cloud',
authentication: { scheme: 'custom', fields: {} },
actions: {
testAction: {
title: 'Test Action',
description: 'Test',
fields: {
userId: { label: 'User ID', description: 'The user ID', type: 'string' }
},
perform: (_request, data) => {
capturedData = data as ExecuteInput<JSONObject, JSONObject>
}
}
}
}

const testDestination = createTestIntegration(destination)
await testDestination.testAction('testAction', {
mapping: { userId: { '@path': '$.userId' } },
event: {
type: 'identify',
userId: 'user-1',
context: {
personas: { computation_class: 'audience', computation_key: 'my_audience' }
},
properties: { my_audience: true }
}
})

expect(capturedData?.audienceMembership).toBe(true)
expect(capturedData?.payload).not.toHaveProperty('audienceMembership')
})
})
100 changes: 100 additions & 0 deletions packages/core/src/audience-membership.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { InputData } from './mapping-kit'
import { SyncMode, AudienceMembership } from './destination-kit/types'

/**
* Resolves whether a user is being added to or removed from an audience.
* Returns `true` if the user is being added, `false` if being removed,
* or `undefined` if the payload is not an audience computation or membership cannot be determined.
*/
export function resolveAudienceMembership(rawData: InputData | undefined, syncMode?: SyncMode): AudienceMembership {
if (!rawData) return undefined

const engageMembership = engageAudienceMembership(rawData)

if (typeof engageMembership === 'boolean') {
return engageMembership
}

const retlMembership = retlAudienceMembership(rawData, syncMode)

if (typeof retlMembership === 'boolean') {
return retlMembership
}

return undefined
}
/**
* Resolves whether a user is being added to or removed from an audience based on Engage data.
* Returns `true` if the user is being added, `false` if being removed,
* or `undefined` if the payload is not an audience computation or membership cannot be determined.
*/
export function engageAudienceMembership(rawData: InputData | undefined): AudienceMembership {
if (!rawData) return undefined

const {
context: {
personas: {
computation_class = '',
computation_key = ''
} = {}
} = {},
properties = {},
traits = {},
type = ''
} = rawData as {
context?: { personas?: { computation_class?: string; computation_key?: string } }
properties?: Record<string, unknown>
traits?: Record<string, unknown>
type?: string
}

if (!['audience', 'journey_step'].includes(computation_class)) return undefined
if (!computation_key) return undefined

let membershipValue: boolean | undefined

if (type === 'identify' && typeof traits?.[computation_key] === 'boolean') {
membershipValue = traits[computation_key]
}
else if (type === 'track' && typeof properties?.[computation_key] === 'boolean') {
membershipValue = properties[computation_key]
}

return typeof membershipValue === 'boolean' ? membershipValue : undefined
}

/**
* Resolves whether a user is being added to or removed from an audience based on RETL data.
* Returns `true` if the user is being added, `false` if being removed,
* or `undefined` if the payload is not an audience computation or membership cannot be determined.
*/
export function retlAudienceMembership(rawData: InputData | undefined, syncMode?: SyncMode): AudienceMembership {
if (!rawData || !syncMode) return undefined

const {
event = '',
type = ''
} = rawData as {
event?: string
type?: string
}

if (type !== 'track') return undefined

if(
(syncMode === 'add' && ['new'].includes(event)) ||
(syncMode === 'update' && ['updated'].includes(event)) ||
(syncMode === 'upsert' && ['new', 'updated'].includes(event)) ||
(syncMode === 'mirror' && ['new', 'updated'].includes(event))
){
return true
}
else if (
(syncMode === 'delete' && ['deleted'].includes(event)) ||
(syncMode === 'mirror' && ['deleted'].includes(event))
){
return false
}

return undefined
}
Loading
Loading