Skip to content

feat(reddit-conversions-api): add Conversions API v3 support behind feature flag - #3848

Open
harsh-joshi99 wants to merge 10 commits into
mainfrom
reddit-conversions-api-v3.0
Open

feat(reddit-conversions-api): add Conversions API v3 support behind feature flag#3848
harsh-joshi99 wants to merge 10 commits into
mainfrom
reddit-conversions-api-v3.0

Conversation

@harsh-joshi99

@harsh-joshi99 harsh-joshi99 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds support for Reddit Conversions API v3 behind the feature flag reddit-conversions-api-canary-version. With the flag off (default), the destination behaves exactly as today (v2.0). With the flag on, events are sent to the v3 endpoint with the v3 payload.

⚠️ This started as a version bump but v3 turned out to be a breaking payload rewrite, not a URL swap. New endpoint, new request envelope, renamed/retyped fields, and a new field. See below.

What changed in v3 (vs v2.0)

Concern v2.0 v3
Endpoint /api/v2.0/conversions/events/{pixel} /api/v3/pixels/{pixel}/conversion_events
Envelope { events, test_mode, partner } { data: { events, partner, test_id } }
event_at RFC3339 string integer, Unix epoch ms
event_type renamed → type
tracking_type PageVisit UPPER_SNAKE (PAGE_VISIT …), CustomCUSTOM
event_metadata renamed → metadata
value_decimal renamed → value
action_source (absent) new (Reddit-required; we default WEBSITE)
test_mode (bool) setting replaced by per-request test_id string
event_source_url, product quantity/item_price (absent) new optional

Approach: flag-gated, isolated modules

  • utils.ts — a single flag fork in send()sendV3 or sendV2.
  • v2.ts — existing v2.0 logic, moved verbatim (unchanged behavior).
  • v3.ts — new v3 payload builder + endpoint.
  • shared.ts — helpers reused by both (hashing, cleaners, field types).

This keeps v2 untouched and makes eventual cleanup a deletion (remove v2.ts, collapse the fork) rather than untangling inline branches.

New fields (additive, optional — safe for existing v2 mappings)

  • action_source — optional, defaults to WEBSITE (Reddit requires it on v3; enforced at runtime in v3.ts). Kept optional to avoid adding a required field to existing actions.
  • test_id — optional; routes to Reddit Event Testing. Only sent on v3.
  • event_source_url, product quantity / item_price — optional.

All new fields are only sent on the v3 path; on v2 they're ignored.

Testing

  • Unit: 30 tests. v2 regression suite unchanged; added v3 payload-shape tests (envelope, epoch-ms event_at, UPPER_SNAKE, type/metadata renames, custom→CUSTOM, test_id), both actions, perform + performBatch.
  • Live (staging): validated against a real test pixel —
    • Flag off → v2.0 request, 200 Successfully processed
    • Flag onPOST /api/v3/pixels/{pixel}/conversion_events, correct v3 body, 200 Successfully processed ✅ (including partner inside data and test_id accepted)

Testing Document

Error handling

Relies on framework defaults, which match Reddit's v3 error semantics: 4xx not retried (except 408/423/429), 5xx retried. Reddit's documented codes (400 validation, 401/403 auth/scope, 429 rate limit, 500 downtime) map cleanly onto these.

Rollout / cleanup

  • Default off; roll out by percentage via the flag; do not dual-send the same event to v2 and v3 (Reddit guidance).
  • On promotion: delete v2.ts, collapse the send() fork, remove the flag + test_mode setting.

Open questions for Reddit (non-blocking, confirming)

  • partner: "SEGMENT" placement inside data — accepted in staging (200); confirming it's the intended location.
  • action_source offline enum value — using PHYSICAL_STORE; confirming exact wire value.

🤖 Generated with Claude Code

- Add REDDIT_CONVERSIONS_CANARY_API_VERSION (v3.0) to versioning-info.ts
- Implement feature flag 'reddit-conversions-api-canary-version' via getApiVersion(features) helper in utils.ts
- Thread features through send() and both actions' perform/performBatch (standardEvent, customEvent)
- testAuthentication intentionally remains on stable v2.0
- Update existing tests to use API_VERSION constant
- Add stable + canary feature-flag tests for both actions
- All 23 tests passing

Breaking changes analysis in breaking-changes-analysis.md (changelog requires manual review — Reddit API host not reachable from build env)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@harsh-joshi99
harsh-joshi99 requested a review from a team as a code owner June 30, 2026 04:28
Copilot AI review requested due to automatic review settings June 30, 2026 04:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.

Upgrades the Reddit Conversions API request URL to support a canary v3.0 endpoint behind the reddit-conversions-api-canary-version feature flag, while keeping stable behavior on v2.0 by default.

Changes:

  • Added stable and canary API version constants and a getApiVersion(features) helper to select the version at runtime.
  • Threaded features through standardEvent/customEvent actions into the shared send() helper.
  • Updated and extended unit tests to validate stable vs canary URL selection.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts Adds a canary v3.0 version constant alongside stable v2.0.
packages/destination-actions/src/destinations/reddit-conversions-api/utils.ts Introduces feature-flag-driven API version selection and threads features into send().
packages/destination-actions/src/destinations/reddit-conversions-api/standardEvent/index.ts Passes features into send() for single and batch sends.
packages/destination-actions/src/destinations/reddit-conversions-api/customEvent/index.ts Passes features into send() for single and batch sends.
packages/destination-actions/src/destinations/reddit-conversions-api/breaking-changes-analysis.md Adds a manual checklist/process doc for validating v3.0 breaking changes.
packages/destination-actions/src/destinations/reddit-conversions-api/tests/index.test.ts Refactors URL assertions to use constants and adds feature-flag URL tests.

Comment on lines +7 to +11
/** REDDIT_CONVERSIONS_CANARY_API_VERSION
* Reddit conversions API version (canary/feature-flagged).
* Testing new version v3.0 behind feature flag.
* API reference: https://ads-api.reddit.com/docs/v2/changelog
*/
Comment thread packages/destination-actions/src/destinations/reddit-conversions-api/utils.ts Outdated
Comment on lines +931 to +933
describe('standardEvent', () => {
it('should use the stable API version by default', async () => {
nock('https://ads-api.reddit.com').post(`/api/${API_VERSION}/conversions/events/ad_account_id_1`).reply(200, {})
Comment on lines +947 to +950
it('should use the canary API version when the feature flag is enabled', async () => {
nock('https://ads-api.reddit.com')
.post(`/api/${CANARY_API_VERSION}/conversions/events/ad_account_id_1`)
.reply(200, {})
Comment on lines +966 to +968
describe('customEvent', () => {
it('should use the stable API version by default', async () => {
nock('https://ads-api.reddit.com').post(`/api/${API_VERSION}/conversions/events/ad_account_id_1`).reply(200, {})
Comment on lines +982 to +985
it('should use the canary API version when the feature flag is enabled', async () => {
nock('https://ads-api.reddit.com')
.post(`/api/${CANARY_API_VERSION}/conversions/events/ad_account_id_1`)
.reply(200, {})
Comment on lines +22 to +26
> **Action required before merge / promoting the canary to stable:** a maintainer must
> manually review the v3.0 changelog at https://ads-api.reddit.com/docs/v2/changelog
> (and any v3 migration guide Reddit publishes) and complete the breaking-changes
> checklist below. Do not enable the feature flag for production traffic until this
> review is done.
…e changelog analysis

- Path segment is 'v3' not 'v3.0' (changelog: /api/v2.0/ -> /api/v3/)
- Complete breaking-changes-analysis.md from the v3 changelog: all
  Conversions API changes v2.0 -> v3 are additive; only the URL path
  version segment changes. Risk downgraded to LOW.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@harsh-joshi99
harsh-joshi99 marked this pull request as draft June 30, 2026 05:25
Adds stable + canary feature-flag tests for the performBatch path on both
standardEvent and customEvent, exercising the features-driven version
selection threaded into send() (addresses PR review feedback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 30, 2026 07:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Comment thread packages/destination-actions/src/destinations/reddit-conversions-api/utils.ts Outdated
Isolate v2 and v3 into separate modules with a single flag fork in utils.ts:
- shared.ts: helpers reused by both (hashing, clean, getAdId, field types)
- v2.ts: existing v2.0 payload + URL, moved verbatim (sendV2)
- v3.ts: v3 payload (sendV3) — /pixels/{id}/conversion_events, { data: { events } }
  envelope, event_at as strict epoch-ms, event_type->type, event_metadata->metadata,
  value_decimal->value, UPPER_SNAKE tracking_type, action_source, event_source_url, test_id
- utils.send() forks on the canary flag; actions unchanged

New optional/defaulted fields (additive, only sent on v3): action_source (required,
default WEBSITE), event_source_url, test_id, product quantity/item_price.

Error handling relies on framework defaults (4xx no-retry except 408/423/429, 5xx retry),
which already match Reddit's v3 error semantics.

Tests: v2 regression suite unchanged; canary URL fixed to the v3 path; added v3
payload-shape tests (envelope, epoch-ms, UPPER_SNAKE, custom->CUSTOM, test_id). 30 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 8, 2026 06:58

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 10 comments.

Comment on lines +24 to +25
const V3_URL = (pixelId: string) =>
`https://ads-api.reddit.com/api/${REDDIT_CONVERSIONS_CANARY_API_VERSION}/pixels/${pixelId}/conversion_events`
Comment on lines +40 to +42
export async function sendV3(request: RequestClient, settings: Settings, payload: StandardEvent[] | CustomEvent[]) {
const data = createRedditPayloadV3(payload)
return request(V3_URL(settings.ad_account_id), {
// v3 requires event_at as an integer Unix epoch in milliseconds. We own the
// timestamp source (defaults to $.timestamp, an ISO string), so we accept ISO
// strings and 13-digit epoch-ms; anything else is rejected rather than sent wrong.
export function toEpochMs(value: string | number | undefined): number {
Comment on lines +94 to +95
if (typeof value === 'number' && Number.isInteger(value)) return value
if (typeof value === 'string' && /^\d{13}$/.test(value.trim())) return Number(value.trim())
Comment thread packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts Outdated
// v3 requires event_at as an integer Unix epoch in milliseconds. We own the
// timestamp source (defaults to $.timestamp, an ISO string), so we accept ISO
// strings and 13-digit epoch-ms; anything else is rejected rather than sent wrong.
export function toEpochMs(value: string | number | undefined): number {
Comment on lines +101 to +104
throw new PayloadValidationError(
`event_at must be an ISO 8601 timestamp or epoch milliseconds, received: ${String(value)}`
)
}
…aking existing actions

action_source is required by Reddit v3 but adding a required field to the existing
standardEvent/customEvent actions is a breaking change (per CLAUDE.md) and would surface
as a required-but-unused field for all v2 users. Make it optional with a WEBSITE default;
enforce presence at runtime in the v3 payload builder instead.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 13, 2026 06:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

const custom_event_name = clean((payload as CustomEvent).custom_event_name)
const tracking_type = custom_event_name ? 'Custom' : (payload as StandardEvent).tracking_type

if (!action_source) throw new PayloadValidationError('action_source is required')
Comment on lines +24 to +25
const V3_URL = (pixelId: string) =>
`https://ads-api.reddit.com/api/${REDDIT_CONVERSIONS_CANARY_API_VERSION}/pixels/${pixelId}/conversion_events`

export async function sendV3(request: RequestClient, settings: Settings, payload: StandardEvent[] | CustomEvent[]) {
const data = createRedditPayloadV3(payload)
return request(V3_URL(settings.ad_account_id), {
@ManikMM

ManikMM commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Manik [Reddit] here,
reviewing the changes now.

@ManikMM

ManikMM commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

@harsh-joshi99 am I allowed to make changes directly to this branch?
Also we should schedule a meeting to walk through the changes, and the segment specific audit we've performed.

… comments

- Delete breaking-changes-analysis.md: written under the initial (wrong) assumption
  that v3 was only a URL version swap; v3 is a full payload rewrite, so the doc is
  inaccurate and misleading.
- index.ts: drop dangling reference to a local-only doc in the test_mode comment.
- versioning-info.ts: correct the canary comment to describe the real v3 endpoint/payload.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 20, 2026 06:05
@harsh-joshi99 harsh-joshi99 changed the title feat(reddit-conversions-api): Upgrade API to v3.0 with feature flag feat(reddit-conversions-api): add Conversions API v3 support behind feature flag Jul 20, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Comment on lines +9 to +11
* v3 is a breaking payload rewrite (new endpoint /api/v3/pixels/{pixel_id}/conversion_events,
* `data` envelope, renamed/retyped fields). See v3.ts for the transform.
* API reference: https://ads-api.reddit.com/docs/v3/capi-migration
Comment on lines +24 to +25
const V3_URL = (pixelId: string) =>
`https://ads-api.reddit.com/api/${REDDIT_CONVERSIONS_CANARY_API_VERSION}/pixels/${pixelId}/conversion_events`

export async function sendV3(request: RequestClient, settings: Settings, payload: StandardEvent[] | CustomEvent[]) {
const data = createRedditPayloadV3(payload)
return request(V3_URL(settings.ad_account_id), {
const custom_event_name = clean((payload as CustomEvent).custom_event_name)
const tracking_type = custom_event_name ? 'Custom' : (payload as StandardEvent).tracking_type

if (!action_source) throw new PayloadValidationError('action_source is required')
}

function createRedditPayloadV3(payloads: StandardEvent[] | CustomEvent[]): V3Payload {
const test_id = clean(payloads[0]?.test_id)
}
})

return { data: { events, partner: 'SEGMENT', test_id } }
if (!dataProcessingOptions) return undefined
return {
country: clean(dataProcessingOptions.country),
modes: dataProcessingOptions.modes?.split(',').map((mode) => mode.trim()),
User,
Product,
EventMetadata,
DatapProcessingOptions

function getDataProcessingOptions(
dataProcessingOptions: DataProcessingOptionsType
): DatapProcessingOptions | undefined {
Copilot AI review requested due to automatic review settings July 20, 2026 06:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Comment on lines +91 to +97
export function toEpochMs(value: string | number | undefined): number {
if (value === undefined || value === null || value === '') {
throw new PayloadValidationError('event_at is required')
}
// Already epoch milliseconds (number or 13-digit numeric string).
if (typeof value === 'number' && Number.isInteger(value)) return value
if (typeof value === 'string' && /^\d{13}$/.test(value.trim())) return Number(value.trim())
Comment on lines +49 to +50
function createRedditPayloadV3(payloads: StandardEvent[] | CustomEvent[]): V3Payload {
const test_id = clean(payloads[0]?.test_id)
Comment on lines +141 to +150
function getDataProcessingOptions(
dataProcessingOptions: DataProcessingOptionsType
): V3DataProcessingOptions | undefined {
if (!dataProcessingOptions) return undefined
return {
country: clean(dataProcessingOptions.country),
modes: dataProcessingOptions.modes?.split(',').map((mode) => mode.trim()),
region: clean(dataProcessingOptions.region)
}
}
Comment on lines +9 to +10
* v3 is a breaking payload rewrite (new endpoint /api/v3/pixels/{pixel_id}/conversion_events,
* `data` envelope, renamed/retyped fields). See v3.ts for the transform.

export const API_VERSION = REDDIT_CONVERSIONS_API_VERSION
export const CANARY_API_VERSION = REDDIT_CONVERSIONS_CANARY_API_VERSION
export const FLAGON_NAME = 'reddit-conversions-api-canary-version'
@harsh-joshi99

Copy link
Copy Markdown
Contributor Author

@harsh-joshi99 am I allowed to make changes directly to this branch? Also we should schedule a meeting to walk through the changes, and the segment specific audit we've performed.

Hey @ManikMM, sorry I missed your comment. I'm not entirely sure if your current permissions allow you to push directly here (cc: @joe-ayoub-segment), but feel free to cut a branch from this one.

Let's connect over the email thread to schedule the meeting.

Copilot AI review requested due to automatic review settings July 20, 2026 12:14

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 5 comments.

Comment on lines +91 to +106
export function toEpochMs(value: string | number | undefined): number {
if (value === undefined || value === null || value === '') {
throw new PayloadValidationError('event_at is required')
}
// Already epoch milliseconds (number or 13-digit numeric string).
if (typeof value === 'number' && Number.isInteger(value)) return value
if (typeof value === 'string' && /^\d{13}$/.test(value.trim())) return Number(value.trim())
// ISO 8601 / RFC3339 string.
if (typeof value === 'string') {
const ms = Date.parse(value)
if (!Number.isNaN(ms)) return ms
}
throw new PayloadValidationError(
`event_at must be an ISO 8601 timestamp or epoch milliseconds, received: ${String(value)}`
)
}
Comment on lines +49 to +50
function createRedditPayloadV3(payloads: StandardEvent[] | CustomEvent[]): V3Payload {
const test_id = clean(payloads[0]?.test_id)
Comment on lines +141 to +150
function getDataProcessingOptions(
dataProcessingOptions: DataProcessingOptionsType
): V3DataProcessingOptions | undefined {
if (!dataProcessingOptions) return undefined
return {
country: clean(dataProcessingOptions.country),
modes: dataProcessingOptions.modes?.split(',').map((mode) => mode.trim()),
region: clean(dataProcessingOptions.region)
}
}
Comment on lines +5 to +12
import {
StandardEventPayloadItem,
StandardEventPayload,
User,
Product,
EventMetadata,
DatapProcessingOptions
} from './types'
Comment on lines +40 to +47
export async function sendV3(request: RequestClient, settings: Settings, payload: StandardEvent[] | CustomEvent[]) {
const data = createRedditPayloadV3(payload)
return request(V3_URL(settings.ad_account_id), {
method: 'POST',
headers: { Authorization: `Bearer ${settings.conversion_token}` },
json: JSON.parse(JSON.stringify(data))
})
}
…cion

toEpochMs accepted any integer as epoch-ms, so a 10-digit epoch-seconds value
would be sent as ms (timestamp in 1970). Require numeric input to be plausibly
ms (>= 1e12) else throw PayloadValidationError. Add unit tests for the rejection
paths (seconds, non-integer, unparseable, missing) plus the accepted cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 20, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.

Comment on lines +49 to +52
function createRedditPayloadV3(payloads: StandardEvent[] | CustomEvent[]): V3Payload {
const test_id = clean(payloads[0]?.test_id)

const events: V3EventItem[] = payloads.map((payload) => {
Comment on lines +85 to +86
return { data: { events, partner: 'SEGMENT', test_id } }
}
Comment on lines +61 to +64
conversion_id,
action_source,
event_source_url
} = payload
Comment on lines +69 to +74
if (!action_source) throw new PayloadValidationError('action_source is required')

return {
event_at: toEpochMs(event_at),
action_source,
event_source_url: clean(event_source_url),
Comment on lines +24 to +25
const V3_URL = (pixelId: string) =>
`https://ads-api.reddit.com/api/${REDDIT_CONVERSIONS_CANARY_API_VERSION}/pixels/${pixelId}/conversion_events`
Comment on lines +40 to +42
export async function sendV3(request: RequestClient, settings: Settings, payload: StandardEvent[] | CustomEvent[]) {
const data = createRedditPayloadV3(payload)
return request(V3_URL(settings.ad_account_id), {
Comment on lines +99 to +103
export interface V3EventItem {
event_at: number
action_source: string
event_source_url?: string
click_id?: string
@harsh-joshi99
harsh-joshi99 marked this pull request as ready for review July 21, 2026 09:21
@github-actions
github-actions Bot requested review from mdkhan-tw and monutwilio July 21, 2026 09:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants