Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
266327b
feat(reddit-conversions-api): add v3 support behind feature flag
joe-ayoub-segment Aug 19, 2026
c92eaa3
refactor(reddit-conversions-api): drop the v2 file split, restore uti…
joe-ayoub-segment Aug 19, 2026
7baa5e4
Move types-v3/utils-v3 into v3 folder, restore test_mode setting
joe-ayoub-segment Aug 19, 2026
489bc41
Centralize v2/v3 version identifiers into versioning-info.ts
joe-ayoub-segment Aug 19, 2026
34257f7
Extract standardEvent action definition into a reusable factory
joe-ayoub-segment Aug 19, 2026
55fa136
Reframe V2 as standard/V3 as beta; restore V2-pinned presets
joe-ayoub-segment Aug 19, 2026
b89918e
Migrate customEvent onto action.ts; drop Flagon flag gating
joe-ayoub-segment Aug 19, 2026
cc9f34b
Add per-item batch validation via MultiStatusResponse for V3; lock do…
joe-ayoub-segment Aug 19, 2026
61c98d7
Restructure createRedditPayloadV3 to mirror the legacy whole-batch flow
joe-ayoub-segment Aug 19, 2026
dde0afc
Dedupe EVENT_TYPE_V3 into TRACKING_TYPE_V3
joe-ayoub-segment Aug 19, 2026
c329461
Dedupe ACTION_SOURCE_V3 into ACTION_SOURCE_V3_LABELS
joe-ayoub-segment Aug 19, 2026
b2a796a
Revert test files to main
joe-ayoub-segment Aug 19, 2026
b3d8999
updating snapshots
joe-ayoub-segment Aug 19, 2026
3e68c88
Add unit tests for Reddit CAPI v3 single-event action and utils-v3 he…
joe-ayoub-segment Aug 19, 2026
d2a97d8
Add batch unit tests for Reddit CAPI v3
joe-ayoub-segment Aug 19, 2026
3e6f685
updating metadata
joe-ayoub-segment Aug 19, 2026
1c1221b
Merge branch 'main' into STRATCONN-6892-reddit-conversions-api-v3
joe-ayoub-segment Aug 19, 2026
989e578
test fixes
joe-ayoub-segment Aug 19, 2026
841ee98
fixing event_metadata bug
joe-ayoub-segment Aug 20, 2026
e563895
Merge branch 'main' into STRATCONN-6892-reddit-conversions-api-v3
joe-ayoub-segment Aug 20, 2026
41b3344
fixing pre-existing mapping bug
joe-ayoub-segment Aug 20, 2026
1dfd6b1
STRATCONN-6892: filter event_metadata by tracking_type per Reddit CAP…
joe-ayoub-segment Aug 21, 2026
474e893
Merge branch 'main' into STRATCONN-6892-reddit-conversions-api-v3
joe-ayoub-segment Aug 21, 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,57 @@

exports[`Testing snapshot for actions-reddit-conversions-api destination: customEvent action - all fields 1`] = `
Object {
"events": Array [
Object {
"click_id": "Q1Q49h4LP@",
"event_at": "2021-02-01T00:00:00.000Z",
"event_metadata": Object {
"conversion_id": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"currency": "HNL",
"item_count": -2840640572358656,
"products": Array [
Object {
"category": "Q1Q49h4LP@",
"id": "Q1Q49h4LP@",
"name": "Q1Q49h4LP@",
},
],
"value_decimal": -28406405723586.56,
},
"event_type": Object {
"custom_event_name": "Q1Q49h4LP@",
"tracking_type": "Custom",
},
"user": Object {
"aaid": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"data_processing_options": Object {
"country": "GM",
"modes": Array [
"LDU",
"data": Object {
"events": Array [
Object {
"action_source": "APP",
"click_id": "Q1Q49h4LP@",
"event_at": 1612137600000,
"event_source_url": "Q1Q49h4LP@",
"metadata": Object {
"conversion_id": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"currency": "HNL",
"item_count": -2840640572358656,
"products": Array [
Object {
"category": "Q1Q49h4LP@",
"id": "Q1Q49h4LP@",
"item_price": -28406405723586.56,
"name": "Q1Q49h4LP@",
"quantity": -2840640572358656,
},
],
"region": "Q1Q49h4LP@",
"value": -28406405723586.56,
},
"email": "8614950525b98787b31febe07634987a801175f098b60c83bd3903bff1ec3eed",
"external_id": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"ip_address": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"phone_number": "781a379f97f79178f343a118a245e179b55a414b1b959ef25376dcb43bfe61f6",
"screen_dimensions": Object {
"height": -2840640572358656,
"width": -2840640572358656,
"type": Object {
"custom_event_name": "Q1Q49h4LP@",
"tracking_type": "CUSTOM",
},
"user": Object {
"aaid": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"data_processing_options": Object {
"country": "GM",
"modes": Array [
"LDU",
],
"region": "Q1Q49h4LP@",
},
"email": "8614950525b98787b31febe07634987a801175f098b60c83bd3903bff1ec3eed",
"external_id": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"ip_address": "4fbe858b31ee34144cc3d009fd4636ef20fb342e095570d49a9898bb8d77598e",
"phone_number": "781a379f97f79178f343a118a245e179b55a414b1b959ef25376dcb43bfe61f6",
"screen_dimensions": Object {
"height": -2840640572358656,
"width": -2840640572358656,
},
"user_agent": "Q1Q49h4LP@",
"uuid": "Q1Q49h4LP@",
},
"user_agent": "Q1Q49h4LP@",
"uuid": "Q1Q49h4LP@",
},
},
],
"partner": "SEGMENT",
"test_mode": true,
],
"partner": "SEGMENT",
"test_id": "Q1Q49h4LP@",
},
}
`;

Expand Down Expand Up @@ -84,7 +90,9 @@ Object {
Object {
"category": "gT6gXh8ga!%xhI",
"id": "gT6gXh8ga!%xhI",
"item_price": 20910002898206.72,
"name": "gT6gXh8ga!%xhI",
"quantity": 2091000289820672,
},
],
"value_decimal": 20910002898206.72,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { getMetadata } from '../utils'

describe('getMetadata', () => {
it('drops currency/value_decimal/item_count for tracking types that don\'t support any event metadata', () => {
const result = getMetadata({ currency: 'USD', item_count: 5, value_decimal: 10 }, undefined, undefined, 'Search')
expect(result?.currency).toBeUndefined()
expect(result?.item_count).toBeUndefined()
expect(result?.value_decimal).toBeUndefined()
})

it('drops item_count but keeps currency/value_decimal for Lead/SignUp', () => {
const result = getMetadata({ currency: 'USD', item_count: 5, value_decimal: 10 }, undefined, undefined, 'Lead')
expect(result?.currency).toBe('USD')
expect(result?.value_decimal).toBe(10)
expect(result?.item_count).toBeUndefined()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import crypto from 'crypto'
import nock from 'nock'
import { createTestEvent, createTestIntegration, JSONObject, SegmentEvent } from '@segment/actions-core'
import Definition from '../index'
import { Settings } from '../generated-types'
import { LEGACY_API_VERSION } from '../versioning-info'

const testDestination = createTestIntegration(Definition)
const timestamp = '2024-01-08T13:52:50.212Z'
const epochMs = 1704721970212
const settings: Settings = {
ad_account_id: 'ad_account_id_1',
conversion_token: 'conversion_token_1'
}

// Matches processHashing('sha256', 'hex', (value) => value.trim()) in ../utils.ts
const sha256 = (value: string) => crypto.createHash('sha256').update(value.trim()).digest('hex')

describe('Reddit Conversions Api - V3 batch events', () => {
it('handles a batch of 10: 2 fail schema validation, 2 fail Reddit-side validation inside performBatch, 6 succeed', async () => {
nock('https://ads-api.reddit.com').post('/api/v3/pixels/ad_account_id_1/conversion_events').reply(200, {})

// Interleaved on purpose so the 6 successes aren't all bunched at one end of the batch -
// 'schemaInvalid' fails Segment's own schema validation (action_source is conditionally required
// when api_version is v3) and never reaches performBatch; 'businessInvalid' passes schema (has
// action_source) but is missing products.id, which is only enforced inside performBatch by
// toProductIdV3; 'valid' should succeed end to end.
const kinds = [
'valid',
'schemaInvalid',
'valid',
'businessInvalid',
'valid',
'schemaInvalid',
'valid',
'businessInvalid',
'valid',
'valid'
] as const

const events: SegmentEvent[] = kinds.map((kind, i) => {
const properties: JSONObject = { revenue: 100 }
if (kind !== 'schemaInvalid') properties.action_source = 'WEBSITE'
properties.products =
kind === 'businessInvalid'
? [{ category: `c${i}`, name: `n${i}` }]
: [{ product_id: `p${i}`, category: `c${i}`, name: `n${i}` }]

return createTestEvent({
timestamp,
event: 'Order Completed',
messageId: `msg-${i}`,
type: 'track',
userId: `user_id_${i}`,
properties
})
})

await testDestination.testBatchAction('standardEvent', {
events,
settings,
useDefaultMappings: true,
mapping: {
tracking_type: 'Purchase',
api_version: 'v3',
action_source: { '@path': '$.properties.action_source' }
}
})

const multistatus = testDestination.results.at(0)?.multistatus
expect(multistatus).toHaveLength(10)

kinds.forEach((kind, i) => {
if (kind === 'schemaInvalid') {
// Schema validation failure (Segment core, before performBatch is ever called).
expect(multistatus?.[i]).toEqual({
status: 400,
errortype: 'PAYLOAD_VALIDATION_FAILED',
errormessage:
'The root value is missing the required field \'action_source\'. The root value must match "then" schema.',
errorreporter: 'INTEGRATIONS'
})
} else if (kind === 'businessInvalid') {
// Reddit-side validation failure (our own code, inside performBatch).
expect(multistatus?.[i]).toEqual({
status: 400,
errortype: 'BAD_REQUEST',
errormessage: 'products.id is required when sending to Reddit Conversions API v3',
errorreporter: 'INTEGRATIONS'
})
} else {
expect(multistatus?.[i]).toEqual({
status: 200,
sent: {
event_at: epochMs,
action_source: 'WEBSITE',
event_source_url: 'https://segment.com/academy/',
type: {
tracking_type: 'PURCHASE'
},
metadata: {
value: 100,
products: [
{
category: `c${i}`,
id: `p${i}`,
name: `n${i}`
}
],
conversion_id: sha256(`msg-${i}`)
},
user: {
external_id: sha256(`user_id_${i}`),
ip_address: sha256('8.8.8.8'),
user_agent:
'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'
}
},
body: { success: true }
})
}
})
})

it('returns a plain response (no MultiStatusResponse) for a pure V2 batch', async () => {
nock('https://ads-api.reddit.com').post('/api/v2.0/conversions/events/ad_account_id_1').reply(200, {})

const events: SegmentEvent[] = [0, 1, 2].map((i) =>
createTestEvent({
timestamp,
event: 'Order Completed',
messageId: `msg-v2-${i}`,
type: 'track',
userId: `user_id_${i}`,
properties: { revenue: 100 }
})
)

await testDestination.testBatchAction('standardEvent', {
events,
settings,
useDefaultMappings: true,
mapping: {
tracking_type: 'Purchase',
api_version: LEGACY_API_VERSION
}
})

// A pure-V2 batch never builds our own MultiStatusResponse - performBatch just returns the
// plain send() response, so core falls back to its own legacy "whole batch response" handling
// (fillMultiStatusResponse), marking every index success with the same status/body.
const multistatus = testDestination.results.at(0)?.multistatus
expect(multistatus).toHaveLength(3)
multistatus?.forEach((entry) => {
expect(entry).toMatchObject({ status: 200, body: {} })
})
})
})
Loading
Loading