Skip to content

STRATCONN-6892 - [Reddit Conversions API] - add v3 support - #3958

Open
joe-ayoub-segment wants to merge 23 commits into
mainfrom
STRATCONN-6892-reddit-conversions-api-v3
Open

STRATCONN-6892 - [Reddit Conversions API] - add v3 support#3958
joe-ayoub-segment wants to merge 23 commits into
mainfrom
STRATCONN-6892-reddit-conversions-api-v3

Conversation

@joe-ayoub-segment

@joe-ayoub-segment joe-ayoub-segment commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What this does

Adds support for Reddit Conversions API v3 alongside the existing v2.0 implementation, as an opt-in, per-mapping choice — v2 stays the default and is completely untouched by this PR.

There is no feature flag. The original plan (per the JIRA ticket) was to gate v3 behind the reddit-conversions-api-canary-version Flagon flag, but that flag has been removed from this implementation — the rollout mechanism is entirely the new Api Version field described below. Flagging this explicitly since it's a scope change from what's on the ticket.

How it works

  • Send Standard Event and Send Custom Event are the only two actions affected. Both now share a factory (action.ts) so the v2/v3 branching logic (resolveVersion, perform/performBatch) lives in one place instead of being duplicated.
  • A new Api Version field on both actions lets a customer choose V2 (default) or V3 (Beta) per mapping. It's a fixed dropdown (no variables/functions/literal text), so a single mapping — and therefore a single batch — is always homogeneously V2 or V3.
  • Existing customers are unaffected. Their saved mappings simply don't have this field set; it resolves to V2 at runtime exactly like today.
  • Opting in: a customer edits a Send Standard Event / Send Custom Event mapping and sets Api Version to V3 (Beta). Doing so surfaces the new Action Source field (required only in that case — see below).
  • The 8 auto-created presets (Page Visit, View Content, Search, Add to Cart, Add to Wishlist, Purchase, Lead, Sign Up) are unchanged and always pinned to V2 — they never opt into V3 automatically.
  • V2 request logic (utils.ts / types.ts) is otherwise unchanged from main, with one deliberate exception: products.quantity/products.item_price were added to v2's wire payload too (see New Fields below). All other v3 logic is new and isolated in v3/utils-v3.ts, v3/types-v3.ts, v3/constants.ts — nothing else is shared between the two versions' payload-building code, so v2 can still be deleted outright later without untangling much.
  • V3 requests go to POST /api/v3/pixels/{ad_account_id}/conversion_events, body wrapped in { data: { events, partner: 'SEGMENT', test_id } }, with event_at sent as epoch milliseconds and tracking_type values in UPPER_SNAKE_CASE (vs v2's PascalCase).
  • V3 batches use a MultiStatusResponse so one bad event doesn't fail the whole batch — but only for validation failures on Segment's side (e.g. a missing product id). If the actual HTTP call to Reddit fails, the whole batch still fails/throws as it always has; Reddit itself has no per-item batch response to parse.

event_metadata is now filtered by tracking_type (both V2 and V3)

Per Reddit's Event Metadata docs, support for currency/value/item_count varies by event type:

  • PageVisit / ViewContent / Search support none of currency/value/item_count (only conversion_id/products are supported for these).
  • Lead / SignUp support currency/value but not item_count.
  • All other types (AddToCart, AddToWishlist, Purchase, Custom) support all three.

Found this via staging diagnostics on real Reddit traffic (Search/View Content events flagged as "unsupported metadata" for carrying currency). Added shared supportsValueMetadata/supportsItemCount helpers in utils.ts, applied in both getMetadata implementations (utils.ts for V2, v3/utils-v3.ts for V3) so a customer can't send unsupported metadata regardless of which version they're on. Unit tests added in __tests__/utils.test.ts (new, V2) and __tests__/v3-utils.test.ts (V3).

New fields

Per-action (Standard Event / Custom Event):

Field Type Notes
Api Version choice (V2 default / V3 (Beta)) Dropdown only, can't be mapped from event data
Action Source choice (WEBSITE/APP/OTHER/PHYSICAL_STORE) Required only when Api Version = V3 (Beta); no default — Reddit (Manik Mehta) explicitly asked us not to guess this per customer
Event Source URL string Defaults to $.context.page.url; only applies to V3
products.quantity / products.item_price integer / number Visible for both V2 and V3 mappings, and sent on the wire for both versions — a deliberate addition to v2's payload, not v3-only

Destination Settings:

Field Notes
Test ID (new) String. Routes V3 events to Reddit Event Testing instead of production. Replaces the old Test Mode boolean for V3 only.
Test Mode (existing, unchanged) Description updated to clarify it's V2-only now.

Testing

  • Added unit tests for new functionality
  • Tested end-to-end using the local server
  • Tested for backward compatibility of destination (existing V2 mappings/presets are unaffected — verified via returns a plain response (no MultiStatusResponse) for a pure V2 batch)
  • Tested in the staging environment

New/changed test files

  • __tests__/v3-single-event.test.tsperform behavior for a single event: sending a Standard/Custom event to V3, staying on V2 when api_version is unset, rejecting the mapping when V3 is selected without action_source, and throwing when a product is missing an id.
  • __tests__/v3-batch-events.test.tsperformBatch behavior: a mixed batch of 10 (schema failures, Reddit-side validation failures, and successes) resolved via MultiStatusResponse, plus confirming a pure-V2 batch still returns a plain response with no MultiStatusResponse.
  • __tests__/v3-utils.test.ts — unit coverage for the v3 helper functions: toEpochMs, toV3TrackingType, toActionSourceV3, toProductIdV3, getProducts, getMetadata, createRedditPayloadV3, and sendV3. Now also covers per-tracking-type metadata filtering.
  • __tests__/utils.test.ts (new) — unit coverage for V2's getMetadata, covering per-tracking-type metadata filtering.
  • __tests__/__snapshots__/snapshot.test.ts.snap — regenerated required/all-fields snapshots to include the new api_version / action_source / event_source_url / product fields (snapshot.test.ts itself is unchanged).

Security Review

  • Reviewed all new field definitions for sensitive data — none of Api Version, Action Source, Event Source URL, or Test ID carry secrets; no new fields need type: 'password'.

Adds Reddit Conversions API v3 support alongside the existing v2.0
implementation, gated behind the reddit-conversions-api-canary-version
Flagon flag plus a new per-action api_version field (default Latest V3,
falls back to Legacy V2 for existing configured customers who never set
it).

- New per-action fields: api_version, action_source (conditionally
  required only when api_version is v3, no default), event_source_url
  (defaults to $.context.page.url), test_id
- products gains quantity/item_price sub-fields
- event_metadata.value_decimal is sent as event_metadata.value on v3;
  event_type renames to type with UPPER_SNAKE_CASE tracking_type values
- v3 request: POST /api/v3/pixels/{ad_account_id}/conversion_events,
  body wrapped in { data: { events, partner, test_id } }, event_at sent
  as epoch milliseconds
- sendV2/sendV3 kept as fully separate, duplicated implementations
  (types-v2.ts/types-v3.ts, utils-v2.ts/utils-v3.ts) so v2 can be
  deleted outright once the migration is complete
- Settings-level test_mode field removed (unused by any customer)

Not included in this PR: replacing the 8 tracking_type presets with
dedicated actions (Page Visit, Purchase, etc.) - deferred for now.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 19, 2026 07:01
…ls.ts/types.ts

Deletes types-v2.ts and utils-v2.ts, and restores utils.ts/types.ts to
their original main-branch content verbatim. v2 stays completely
untouched; v3 is purely additive in types-v3.ts/utils-v3.ts, which now
also owns the FLAGON_NAME/isCanary/resolveVersion routing helpers.

standardEvent/customEvent call the original send() for the v2 path and
sendV3() for v3, per resolveVersion(payload.api_version, features).

Known follow-up: utils.ts still references settings.test_mode, which
no longer exists on Settings (removed earlier). Left as-is per
instruction not to worry about lint/type checks for this push.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

New required fields detected

Warning

Your PR adds new required fields to an existing destination. Adding new required settings/mappings for a destination already in production requires updating existing customer destination configuration. Ignore this warning if this PR is for a new destination with no active customers in production.

The following required fields were added in this PR:

  • Destination: Reddit Conversions API, Action Field(s):action_source
  • Destination: Reddit Conversions API, Action Field(s):action_source

Add these new fields as optional instead and assume default values in perform or performBatch block.

- Move types-v3.ts and utils-v3.ts into a new v3/ subfolder, fixing
  their relative imports and all call sites
- Re-add the test_mode Settings field (removed earlier, but still
  referenced by v2 utils.ts) and regenerate generated-types.ts +
  metadata.json
- Update test_mode's description to clarify it's V2-only (deprecated)
  and point to the per-action Test ID field for V3 test events

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Adds gated Reddit Conversions API v3 support alongside the existing v2 integration, including new per-action fields and v3-specific payload shaping/routing behind a Flagon canary.

Changes:

  • Introduces v3 types + request builder/sender and routes events to v2 vs v3 based on feature flag + per-action api_version.
  • Adds new action fields for v3 (api_version, action_source, event_source_url, test_id) and extends products item schema.
  • Removes unused destination-level test_mode and updates/extends Jest coverage for v3 behavior.

Reviewed changes

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

Show a summary per file
File Description
packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts Adds explicit constant for canary v3 API version.
packages/destination-actions/src/destinations/reddit-conversions-api/utils-v3.ts Implements v3 URL, payload shaping, hashing/cleaning, version resolution, and sender.
packages/destination-actions/src/destinations/reddit-conversions-api/types-v3.ts Defines wire-level v3 payload interfaces.
packages/destination-actions/src/destinations/reddit-conversions-api/standardEvent/index.ts Routes standard events to v2 vs v3; adds new v3 fields to action.
packages/destination-actions/src/destinations/reddit-conversions-api/customEvent/index.ts Routes custom events to v2 vs v3; adds new v3 fields to action.
packages/destination-actions/src/destinations/reddit-conversions-api/*/generated-types.ts Updates generated payload typings for new fields and product sub-fields.
packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts Adds new v3 mapping fields and extends products schema.
packages/destination-actions/src/destinations/reddit-conversions-api/index.ts Removes destination-level test_mode and removes multiple presets.
packages/destination-actions/src/destinations/reddit-conversions-api/tests/index.test.ts Updates v2 assertions and adds v3 (canary) routing tests.
packages/destination-actions/src/destinations/reddit-conversions-api/**/snapshots/*.snap Updates snapshots for test_mode removal and adds additional snapshots.
Suppressed comments (2)

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:8

  • The PR description says replacing the 8 tracking_type presets with dedicated actions is deferred, but this diff removes multiple presets from the destination definition. If this is intentional (e.g., presets moved/renamed elsewhere in this PR), the description should be updated to match; otherwise, the removed presets should be restored to avoid an unintended behavior change for users relying on them.
const destination: DestinationDefinition<Settings> = {

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:82

  • The PR description says replacing the 8 tracking_type presets with dedicated actions is deferred, but this diff removes multiple presets from the destination definition. If this is intentional (e.g., presets moved/renamed elsewhere in this PR), the description should be updated to match; otherwise, the removed presets should be restored to avoid an unintended behavior change for users relying on them.
  presets: [

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread packages/destination-actions/src/destinations/reddit-conversions-api/utils-v3.ts Outdated
Comment thread packages/destination-actions/src/destinations/reddit-conversions-api/utils-v3.ts Outdated
Copilot AI review requested due to automatic review settings August 19, 2026 07:18
- Replace raw 'v2'/'v3' string literals with named constants
  (LEGACY_API_VERSION, LATEST_API_VERSION) exported from
  versioning-info.ts, used consistently across fields.ts,
  resolveVersion(), and both action perform/performBatch handlers
- LEGACY_API_VERSION reuses the existing v2.0 URL-segment constant
  rather than introducing a separate 'v2' identifier

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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 16 out of 16 changed files in this pull request and generated 2 comments.

Suppressed comments (6)

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:35

  • The PR description says the settings-level test_mode field was removed, but it still exists in the destination settings schema (and in metadata/generated types). Either remove it end-to-end (schema + metadata + any v2 payload usage) or update the PR description to reflect that it's only deprecated (not removed).
        label: '[Deprecated] Test Mode',
        description:
          'Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2, which is deprecated - V3 is the latest API version. To send test events on V3, set the Test ID field on an action mapping instead.',
        type: 'boolean',
        required: false,
        default: false

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:229

  • canonicalizeEmail assumes the input contains exactly one '@'. If an email value is present but malformed (no '@' or trailing '@'), localPartAndDomain[1] will be undefined and this will produce an invalid canonical email (or throw when calling .toLowerCase()). Add a guard (e.g., require exactly 2 parts and a non-empty domain) and fall back to a safer normalization (trim + lowercase) or skip hashing for invalid emails.
function canonicalizeEmail(value: string): string {
  value = value.trim()
  const localPartAndDomain = value.split('@')
  const localPart = localPartAndDomain[0].replace(/\./g, '').split('+')[0]
  return `${localPart.toLowerCase()}@${localPartAndDomain[1].toLowerCase()}`
}

packages/destination-actions/src/destinations/reddit-conversions-api/v3/types-v3.ts:50

  • action_source is treated as required by the v3 sender (it throws if missing) and is conditionally required by the field schema for v3. Keeping it optional in V3EventItem weakens type-safety and can let incorrect callers compile. Make action_source required in V3EventItem (and optionally narrow it to the allowed enum values) to align the types with actual runtime requirements.
export interface V3EventItem {
  event_at: number
  action_source?: string
  event_source_url?: string
  click_id?: string
  type: {
    tracking_type: string
    custom_event_name?: string
  }
  event_metadata?: V3Metadata
  user?: V3User
}

packages/destination-actions/src/destinations/reddit-conversions-api/standardEvent/index.ts:53

  • The batch router filters the same payload array twice and re-runs resolveVersion for each event two times. This can be simplified into a single pass partition (build v2Payloads and v3Payloads in one loop) to reduce repeated work and keep the routing logic in one place. The same pattern appears in customEvent/index.ts.
  performBatch: async (request, { settings, payload, features }) => {
    const v2Payloads = payload.filter((p) => resolveVersion(p.api_version, features) === LEGACY_API_VERSION)
    const v3Payloads = payload.filter((p) => resolveVersion(p.api_version, features) === LATEST_API_VERSION)
    const requests = []
    if (v2Payloads.length) requests.push(send(request, settings, v2Payloads))
    if (v3Payloads.length) requests.push(sendV3(request, settings, v3Payloads))
    return Promise.all(requests)
  }

packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts:3

  • The earlier doc comment with the Reddit API reference link was removed. Since these constants define externally-versioned behavior, re-adding a brief comment/link (e.g., v2 changelog and v3 docs) would help future maintainers validate version semantics and deprecation timelines.
export const LEGACY_API_VERSION = 'v2.0'
export const LATEST_API_VERSION = 'v3'
export type ApiVersion = typeof LEGACY_API_VERSION | typeof LATEST_API_VERSION

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:134

  • The new toEpochMs validation has multiple important branches (epoch-ms number, epoch-ms numeric string, ISO string, and rejection of epoch seconds via EPOCH_MS_MIN). Current integration tests cover the ISO-string path, but not the numeric-string/number paths or the rejection behavior. Add focused unit tests (or extend existing tests) to cover: (1) numeric epoch-ms string input, (2) integer epoch-ms number input, and (3) a 10-digit epoch-seconds value being rejected with the expected error.
const EPOCH_MS_MIN = 1e12
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 numeric string), only if plausibly ms.
  if (typeof value === 'number' && Number.isInteger(value) && value >= EPOCH_MS_MIN) return value
  if (typeof value === 'string' && /^\d+$/.test(value.trim()) && Number(value.trim()) >= EPOCH_MS_MIN) {
    return Number(value.trim())
  }
  // ISO 8601 / RFC3339 string.
  if (typeof value === 'string' && !/^\d+$/.test(value.trim())) {
    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)}`
  )
}

Copilot AI review requested due to automatic review settings August 19, 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 16 out of 16 changed files in this pull request and generated 2 comments.

Suppressed comments (5)

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:134

  • The new toEpochMs behavior has multiple important branches (epoch-ms numeric string, epoch-seconds rejection via EPOCH_MS_MIN, invalid timestamps) but the added tests only appear to cover the ISO-string path. Please add unit tests covering: (1) numeric epoch-ms string, (2) 10-digit epoch-seconds string/number being rejected, and (3) invalid timestamp strings producing the expected validation error.
const EPOCH_MS_MIN = 1e12
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 numeric string), only if plausibly ms.
  if (typeof value === 'number' && Number.isInteger(value) && value >= EPOCH_MS_MIN) return value
  if (typeof value === 'string' && /^\d+$/.test(value.trim()) && Number(value.trim()) >= EPOCH_MS_MIN) {
    return Number(value.trim())
  }
  // ISO 8601 / RFC3339 string.
  if (typeof value === 'string' && !/^\d+$/.test(value.trim())) {
    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)}`
  )
}

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:194

  • modes can become [''] when the input is an empty string or contains trailing commas (e.g. 'LDU,'), which may be an invalid value to send downstream. Consider trimming and filtering empty tokens (e.g. split -> trim -> filter(Boolean)), and returning undefined (or an empty array, depending on Reddit's schema) when no valid modes remain.
  return {
    country: clean(dataProcessingOptions.country),
    modes: dataProcessingOptions.modes?.split(',').map((mode) => mode.trim()),
    region: clean(dataProcessingOptions.region)
  }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:64

  • Serializing and parsing the payload (JSON.parse(JSON.stringify(data))) adds overhead and can mask issues by coercing values (and would drop non-JSON types if introduced later). Prefer passing the plain object directly as json: data. If the intent is to omit undefined keys, consider using a dedicated, explicit sanitizer that preserves types while removing only undefined.
  return request(V3_URL(settings.ad_account_id), {
    method: 'POST',
    headers: { Authorization: `Bearer ${settings.conversion_token}` },
    json: JSON.parse(JSON.stringify(data))
  })

packages/destination-actions/src/destinations/reddit-conversions-api/v3/types-v3.ts:41

  • action_source is treated as required in createRedditPayloadV3 (it throws if missing), but the type marks it optional. Making this field required in V3EventItem will align compile-time guarantees with runtime behavior and reduce the chance of accidentally constructing invalid v3 payloads in future refactors.
export interface V3EventItem {
  event_at: number
  action_source?: string
  event_source_url?: string
  click_id?: string

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:35

  • The PR description states: "Settings-level test_mode field removed". In the changes shown, test_mode still exists as a destination setting (now labeled deprecated). Please either (a) update the PR description to reflect that test_mode remains but is deprecated, or (b) remove the setting from the destination definition and generated types if the intent is to fully remove it.
        label: '[Deprecated] Test Mode',
        description:
          'Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2, which is deprecated - V3 is the latest API version. To send test events on V3, set the Test ID field on an action mapping instead.',
        type: 'boolean',
        required: false,
        default: false

- Add action.ts exporting standardEventAction(trackingType, title,
  description, defaultSubscription?) - builds the shared fields/
  perform/performBatch logic for the standardEvent action family.
  When trackingType is set, tracking_type is hardcoded and hidden
  from the UI (for future preset-replacement actions); when
  undefined, tracking_type stays user-selectable (current
  standardEvent behavior)
- standardEvent/index.ts is now a one-line call into the factory
- Move isCanary/resolveVersion from v3/utils-v3.ts into action.ts
  (FLAGON_NAME stays in v3/utils-v3.ts); customEvent/index.ts now
  imports resolveVersion from ../action instead

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 19, 2026 08:11
- api_version field: default flips to LEGACY_API_VERSION (V2);
  choices/descriptions across fields.ts and test_mode reworded to
  frame V2 as the current standard and V3 as beta, not "legacy vs
  latest"
- Restore the 8 tracking_type presets (Page Visit, View Content,
  Search, Add to Cart, Add to Wishlist, Purchase, Lead, Sign Up)
  removed earlier in this branch, matching main's mapping shape but
  with api_version explicitly pinned to LEGACY_API_VERSION so they
  always resolve to V2 regardless of the Flagon flag state
- Regenerate generated-types.ts for root/standardEvent/customEvent
  to match the description wording changes

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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 17 out of 17 changed files in this pull request and generated no new comments.

Suppressed comments (7)

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:50

  • The PR description states the per-action api_version default should be “Latest v3” (with existing customers staying on v2 because the stored mapping has no value), but the field default is currently LEGACY_API_VERSION (v2.0). Either update the description, or change the default (and any templates/default mappings that intentionally pin v2) so new mappings default to v3 while “missing field” continues to resolve to v2.
export const api_version: InputField = {
  label: 'API Version',
  description:
    'The version of the Reddit Conversions API to send this event to. "V3 (Beta)" requires Action Source to be set.',
  type: 'string',
  required: false,
  default: LEGACY_API_VERSION,
  choices: [
    { label: 'V3 (Beta)', value: LATEST_API_VERSION },
    { label: 'V2', value: LEGACY_API_VERSION }
  ]

packages/destination-actions/src/destinations/reddit-conversions-api/v3/types-v3.ts:41

  • action_source is treated as required for v3 at runtime (payload creation throws when missing), but the v3 wire type marks it optional. Making this property required in V3EventItem will better reflect the API contract and prevent accidental creation of invalid v3 event objects in future changes.
export interface V3EventItem {
  event_at: number
  action_source?: string
  event_source_url?: string
  click_id?: string

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:209

  • canonicalizeEmail assumes the email contains exactly one @ and will produce a malformed canonical form (or throw when calling .toLowerCase() on undefined) if the input is missing a domain part. Consider validating the split result and either (a) return a conservative normalized value (e.g., value.toLowerCase()) or (b) skip hashing/canonicalization for invalid emails to avoid sending incorrect hashed identifiers to Reddit.
function canonicalizeEmail(value: string): string {
  value = value.trim()
  const localPartAndDomain = value.split('@')
  const localPart = localPartAndDomain[0].replace(/\./g, '').split('+')[0]
  return `${localPart.toLowerCase()}@${localPartAndDomain[1].toLowerCase()}`
}

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:112

  • The EPOCH_MS_MIN = 1e12 guard will reject legitimate historical timestamps before ~2001-09-09, and also rejects common “epoch seconds” inputs rather than converting them. If backfills or user-provided timestamps are realistic here, consider accommodating 10-digit seconds (convert to ms) or lowering the minimum bound so you prevent 1970-misreads without dropping otherwise valid events.
// 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 epoch-ms; anything else
// is rejected rather than sent wrong. EPOCH_MS_MIN guards against epoch *seconds* being misread
// as ms (a 10-digit seconds value is < 1e12, so it's rejected instead of landing in 1970).
const EPOCH_MS_MIN = 1e12
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 numeric string), only if plausibly ms.
  if (typeof value === 'number' && Number.isInteger(value) && value >= EPOCH_MS_MIN) return value
  if (typeof value === 'string' && /^\d+$/.test(value.trim()) && Number(value.trim()) >= EPOCH_MS_MIN) {
    return Number(value.trim())
  }

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:83

  • The v2/v3 split logic is duplicated here and in customEvent/index.ts. Consider extracting a small helper (e.g., “partitionByResolvedVersion” or “buildVersionedRequests”) to keep routing behavior consistent as fields/versions evolve and reduce the chance of drift between Standard and Custom event actions.
    performBatch: async (request, { settings, payload, features }) => {
      const resolvedPayloads = trackingType ? payload.map((p) => ({ ...p, tracking_type: trackingType })) : payload
      const v2Payloads = resolvedPayloads.filter((p) => resolveVersion(p.api_version, features) === LEGACY_API_VERSION)
      const v3Payloads = resolvedPayloads.filter((p) => resolveVersion(p.api_version, features) === LATEST_API_VERSION)
      const requests = []
      if (v2Payloads.length) requests.push(send(request, settings, v2Payloads))
      if (v3Payloads.length) requests.push(sendV3(request, settings, v3Payloads))
      return Promise.all(requests)

packages/destination-actions/src/destinations/reddit-conversions-api/metadata.json:31

  • metadata.json marks test_mode as deprecated, but the runtime destination settings definition in index.ts still labels it simply “Test Mode” (and generated-types.ts still includes it). To avoid confusing customers, align the label/description between the catalog metadata and the actual settings UI definition (or remove the setting entirely if that’s the intended outcome).
        "label": "[Deprecated] Test Mode",
        "description": "Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2, which is deprecated - V3 is the latest API version. To send test events on V3, set the Test ID field on an action mapping instead.",
        "type": "boolean",
        "required": false,
        "multiple": false,

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:114

  • The new toEpochMs behavior has several branches (ISO parsing, numeric string parsing, integer ms validation, and error cases). The current v3 tests cover the ISO-to-epoch-ms path, but adding targeted unit tests for numeric string ms input and rejection/error cases would help prevent regressions in timestamp handling.
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 numeric string), only if plausibly ms.
  if (typeof value === 'number' && Number.isInteger(value) && value >= EPOCH_MS_MIN) return value
  if (typeof value === 'string' && /^\d+$/.test(value.trim()) && Number(value.trim()) >= EPOCH_MS_MIN) {
    return Number(value.trim())
  }
  // ISO 8601 / RFC3339 string.
  if (typeof value === 'string' && !/^\d+$/.test(value.trim())) {
    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)}`
  )
}

Copilot AI review requested due to automatic review settings August 19, 2026 08: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

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

Suppressed comments (3)

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:1

  • The PR description states that the settings-level test_mode field is removed, but it remains exposed in the destination settings (and is also still present in generated settings types and referenced by v2 payload building). If the intention is deprecation (as metadata.json suggests), update the PR description accordingly; if the intention is true removal, the settings schema and any remaining usage in v2 payload construction should be removed in this PR.
import { defaultValues, DestinationDefinition } from '@segment/actions-core'

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:174

  • When dataProcessingOptions.modes is an empty string (or contains extra commas), this produces [''] (or includes empty entries), which can be invalid/noisy on the wire. Consider filtering out falsy/empty trimmed modes after splitting so the API sees only meaningful values.
  return {
    country: clean(dataProcessingOptions.country),
    modes: dataProcessingOptions.modes?.split(',').map((mode) => mode.trim()),
    region: clean(dataProcessingOptions.region)
  }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/types-v3.ts:41

  • action_source is enforced as required in createRedditPayloadV3 (it throws when missing), but the type marks it optional. This mismatch weakens type-safety and makes it easier for future refactors to accidentally construct invalid payloads. Consider making action_source: string required on V3EventItem (and/or adjusting upstream payload typing) to match actual requirements.
export interface V3EventItem {
  event_at: number
  action_source?: string
  event_source_url?: string

- customEvent/index.ts is now a one-line call into a new
  customEventAction(title, description, defaultSubscription?)
  factory in action.ts, sharing buildEventAction's fields/perform/
  performBatch logic with standardEventAction
- buildEventAction takes two named optional fields (trackingTypeField,
  customEventNameField) instead of a generic fields bag - exactly one
  is passed per action variant, the other stays undefined and is
  omitted from fields
- resolveVersion no longer checks the Flagon canary flag - version
  selection is based purely on the payload's api_version value now.
  Removed isCanary/FLAGON_NAME (dead code) and the now-meaningless
  features:{...} from test payloads

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 19, 2026 08:48

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 17 out of 17 changed files in this pull request and generated 1 comment.

Suppressed comments (8)

packages/destination-actions/src/destinations/reddit-conversions-api/tests/index.test.ts:1

  • Tests set api_version: 'v2', but your defined legacy version constant/choice value is 'v2.0'. Using 'v2' may not reflect real configured mappings produced by the UI/choices and can hide issues if strict validation is later added. Prefer using the actual legacy value ('v2.0') or import LEGACY_API_VERSION in tests.
import nock from 'nock'

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:44

  • test_id is lifted only from the first payload in a batch; if later payloads have a different test_id (or unset), the request will silently use the first value for all events. Since v3 places test_id at the request level, consider validating that all payloads have the same test_id (or all unset) and throw a PayloadValidationError if they differ.
function createRedditPayloadV3(payloads: StandardEvent[] | CustomEvent[]): V3Payload {
  const test_id = clean((payloads[0] as StandardEvent | CustomEvent)?.test_id)

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:168

  • Splitting modes and trimming can produce empty strings (e.g., "LDU," => ["LDU", ""]), which may be rejected downstream. Filter out empty entries after trimming (and consider omitting modes entirely if it becomes an empty array).
  return {
    country: clean(dataProcessingOptions.country),
    modes: dataProcessingOptions.modes?.split(',').map((mode) => mode.trim()),
    region: clean(dataProcessingOptions.region)
  }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:203

  • canonicalizeEmail assumes the input contains exactly one @. For invalid inputs (missing @, multiple @), localPartAndDomain[1] can be undefined, producing a corrupted canonical form and potentially hashing the wrong value. Add a guard (e.g., if the split doesn’t yield exactly 2 parts) to fall back to a safe normalization (trim/lowercase) or to throw a validation error.
function canonicalizeEmail(value: string): string {
  value = value.trim()
  const localPartAndDomain = value.split('@')
  const localPart = localPartAndDomain[0].replace(/\./g, '').split('+')[0]
  return `${localPart.toLowerCase()}@${localPartAndDomain[1].toLowerCase()}`
}

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:73

  • These casts to StandardEvent/StandardEvent[] are incorrect for the custom event action path and reduce type safety (e.g., custom_event_name becomes invisible to the compiler). Keep the values typed as the generic Payload (or as (StandardEvent|CustomEvent)) so TypeScript can validate both variants and you don’t accidentally mask future changes.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload])
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]
      const v2Payloads = resolvedPayloads.filter((p) => resolveVersion(p.api_version) === LEGACY_API_VERSION)
      const v3Payloads = resolvedPayloads.filter((p) => resolveVersion(p.api_version) === LATEST_API_VERSION)

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:80

  • The new batching behavior can fan out into two HTTP requests (one for v2 payloads and one for v3 payloads). Add a unit test that sends a mixed-version batch and asserts that two requests are made with the correct endpoints/bodies (and that the returned Promise.all shape is what callers expect).
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]
      const v2Payloads = resolvedPayloads.filter((p) => resolveVersion(p.api_version) === LEGACY_API_VERSION)
      const v3Payloads = resolvedPayloads.filter((p) => resolveVersion(p.api_version) === LATEST_API_VERSION)
      const requests = []
      if (v2Payloads.length) requests.push(send(request, settings, v2Payloads))
      if (v3Payloads.length) requests.push(sendV3(request, settings, v3Payloads))
      return Promise.all(requests)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/types-v3.ts:41

  • In the v3 sender, action_source is treated as required (it throws if missing). Marking it optional in V3EventItem makes the type inconsistent with actual runtime requirements and reduces compile-time guarantees. Consider making action_source required in V3EventItem.
export interface V3EventItem {
  event_at: number
  action_source?: string
  event_source_url?: string

packages/destination-actions/src/destinations/reddit-conversions-api/metadata.json:31

  • The PR description says the settings-level test_mode field was removed, but the destination metadata still exposes it (now marked deprecated). Either update the PR description to reflect that it’s deprecated (not removed) or remove it from metadata/settings definitions if the intent is full removal.
        "label": "[Deprecated] Test Mode",
        "description": "Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2, which is deprecated - V3 is the latest API version. To send test events on V3, set the Test ID field on an action mapping instead.",

…wn api_version and test_id

- sendV3 now takes raw payloads + an isBatch flag: for a single event
  it throws on invalid input as before; for a batch it validates each
  payload independently (via the merged createRedditPayloadV3), sends
  only the valid ones in one request, and reports per-item validation
  errors through a MultiStatusResponse instead of failing the whole
  batch. V2 batches are unaffected - homogeneous per-mapping
  api_version means a batch is never mixed V2/V3
- Add ProductV3.id required-id validation (throws if missing/blank)
  now that the type requires it; V2's getProducts gains matching
  quantity/item_price mapping
- New v3/constants.ts: ACTION_SOURCE_V3 (+ labels) and EVENT_TYPE_V3,
  used to type-narrow action_source/tracking_type in types-v3.ts and
  to generate the action_source field's choices in fields.ts
- api_version field: disabledInputMethods locks it to the choices
  dropdown only - customers can no longer map it from event payload
  data
- test_id moved from an action-level field to a destination-level
  Settings field (settings are never payload-mapped, so this was a
  cleaner way to guarantee literal-only entry than restricting the
  action field directly)
- Consolidated duplicate helpers (clean, cleanNum, getAdId,
  getDataProcessingOptions, getScreen, getUser, canonicalizeEmail,
  smartHash, cleanPhoneNumber) into utils.ts, exported and reused
  from v3/utils-v3.ts instead of being duplicated

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 19, 2026 10:37
joe-ayoub-segment and others added 2 commits August 19, 2026 11:41
createRedditPayloadV3 now takes the whole payloads array (plus settings,
multiStatusResponse, isBatch) and builds the full PayloadV3 envelope
internally - looping, validating, and populating the MultiStatusResponse
per item - the same shape as v2's createRedditPayload(payloads, settings).
sendV3 is simplified to: build the payload, POST if there's anything to
send, and return appropriately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EVENT_TYPE_V3 was only ever used to type TRACKING_TYPE_V3's values and
derive EventTypeV3 - never as an array in its own right, and its values
duplicated TRACKING_TYPE_V3's values exactly. Made TRACKING_TYPE_V3
`as const` and derive EventTypeV3 from its values directly instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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 19 out of 19 changed files in this pull request and generated no new comments.

Suppressed comments (8)

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:53

  • The PR description says the per-action api_version defaults to Latest (v3) for new mappings, falling back to legacy v2 only for existing customers who never set it. This field currently defaults to LEGACY_API_VERSION, which will keep new mappings on v2 by default; either update the default to LATEST_API_VERSION (and handle the “existing mapping missing the field” case separately) or update the PR description to match the implemented behavior.
export const api_version: InputField = {
  label: 'API Version',
  description:
    'The version of the Reddit Conversions API to send this event to. "V3 (Beta)" requires Action Source to be set.',
  type: 'string',
  required: false,
  default: LEGACY_API_VERSION,
  choices: [
    { label: 'V3 (Beta)', value: LATEST_API_VERSION },
    { label: 'V2', value: LEGACY_API_VERSION }
  ],
  disabledInputMethods: ['literal', 'variable', 'function', 'freeform', 'enrichment']
}

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:43

  • The PR description states “Settings-level test_mode field removed” and also calls out a new per-action test_id field, but this change set keeps test_mode in destination settings and introduces test_id as a settings-level field. Please align the implementation with the PR description (remove test_mode from settings and/or add test_id at the action level), or update the PR description to reflect the actual design.
      test_mode: {
        label: 'Test Mode',
        description:
          'Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2. V3 (Beta) is the latest API version. To send test events on V3, set the Test ID setting instead.',
        type: 'boolean',
        required: false,
        default: false
      },
      test_id: {
        label: 'Test ID',
        description:
          'A test ID from Reddit Event Testing. When set, events are routed to Event Testing for verification instead of production. Remove before sending production traffic. Only applies to Reddit Conversions API V3 (Beta).',
        type: 'string',
        required: false
      }

packages/destination-actions/src/destinations/reddit-conversions-api/utils.ts:94

  • quantity and item_price are now included in the v2 payload shaping as well. The PR description indicates these additions are for v3, so including them in v2 requests risks breaking v2 if the endpoint rejects unknown fields. Recommendation: only add these fields in the v3 transformer (or conditionally omit them when sending v2).
    return {
      category: clean(product.category),
      id: clean(product.id),
      name: clean(product.name),
      quantity: cleanNum(product.quantity),
      item_price: cleanNum(product.item_price)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • buildEventAction is generic over Payload extends StandardEvent | CustomEvent, but it force-casts both single and batch payloads to StandardEvent. This weakens type-safety for the customEvent action (and can mask shape issues in future edits). Suggestion: preserve the generic Payload type (or cast to StandardEvent | CustomEvent) and pass that through to send/sendV3, which already accept both payload shapes.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

      // api_version is a static per-mapping setting, not derived from event data, so a batch is
      // always homogeneously all-V2 or all-V3 - checking the first payload is enough.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:63

  • The indices array is unnecessary here, and using indices.indexOf(index) adds an avoidable linear lookup for every payload (O(n²) overall). You can remove indices entirely and set sent directly to event (or events[events.length - 1]) since you already have the event object in scope.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {
    try {
      const {
        event_at,
        click_id,
        products,
        user,
        data_processing_options,
        screen_dimensions,
        event_metadata,
        conversion_id,
        action_source,
        event_source_url
      } = payload

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • The indices array is unnecessary here, and using indices.indexOf(index) adds an avoidable linear lookup for every payload (O(n²) overall). You can remove indices entirely and set sent directly to event (or events[events.length - 1]) since you already have the event object in scope.
      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/metadata.json:31

  • This metadata marks test_mode as “[Deprecated]”, but index.ts still presents the settings field label as “Test Mode” (non-deprecated). Please make the labels/descriptions consistent across metadata.json and the destination definition so the UI and generated docs don’t disagree about deprecation status.
        "label": "[Deprecated] Test Mode",
        "description": "Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2, which is deprecated - V3 is the latest API version. To send test events on V3, set the Test ID field on an action mapping instead.",
        "type": "boolean",
        "required": false,
        "multiple": false,

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:118

  • toEpochMs contains several important parsing branches and validation rules (epoch ms number, epoch ms digit string, ISO 8601 string, invalid inputs). Current v3 tests cover the ISO string path but not the other branches. Please add focused unit tests for: digit-string epoch ms, numeric epoch ms, rejection of epoch seconds, and an invalid string to ensure error messaging is stable.
export function toEpochMs(value: string | number | undefined): number {
  const EPOCH_MS_MIN = 1e12
  if (value === undefined || value === null || value === '') {
    throw new PayloadValidationError('event_at is required')
  }
  if (typeof value === 'number' && Number.isInteger(value) && value >= EPOCH_MS_MIN) return value
  if (typeof value === 'string') {
    const trimmed = value.trim()
    const isDigitsOnly = /^\d+$/.test(trimmed)
    if (isDigitsOnly && Number(trimmed) >= EPOCH_MS_MIN) return Number(trimmed)
    if (!isDigitsOnly) {
      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)}`
  )
}

Copilot AI review requested due to automatic review settings August 19, 2026 10:54
Same pattern as the earlier EVENT_TYPE_V3/TRACKING_TYPE_V3 dedupe:
ACTION_SOURCE_V3_LABELS was already keyed by the same values ACTION_SOURCE_V3
held, so the array was pure duplication. Derive ActionSourceV3 from the
labels object's keys instead, build fields.ts's choices via
Object.entries(), and validate via the `in` operator.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

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 21 out of 21 changed files in this pull request and generated 3 comments.

Suppressed comments (7)

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:53

  • The PR description says api_version defaults to “Latest V3” with fallback to legacy v2 for existing customers who never set it, but the field default here is LEGACY_API_VERSION (v2.0) and resolveVersion() routes missing/unknown values to v2.0. Either update the PR description to match the shipped behavior, or adjust the implementation to default new mappings to v3 while still routing truly “missing field” legacy mappings to v2 (e.g., by distinguishing “undefined because field didn’t exist” from “defaulted” and/or by only applying a v3 default in UI/templates rather than at runtime).
export const api_version: InputField = {
  label: 'API Version',
  description:
    'The version of the Reddit Conversions API to send this event to. "V3 (Beta)" requires Action Source to be set.',
  type: 'string',
  required: false,
  default: LEGACY_API_VERSION,
  choices: [
    { label: 'V3 (Beta)', value: LATEST_API_VERSION },
    { label: 'V2', value: LEGACY_API_VERSION }
  ],
  disabledInputMethods: ['literal', 'variable', 'function', 'freeform', 'enrichment']
}

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:118

  • EPOCH_MS_MIN = 1e12 rejects valid epoch-millisecond timestamps prior to ~2001-09-09 (e.g., 946684800000 for 2000-01-01). If the contract is “epoch milliseconds”, the validator should accept any reasonable ms value and only reject values that look like epoch seconds (commonly 10 digits) or otherwise invalid. A more robust approach is to infer seconds vs ms by digit length / magnitude (e.g., treat 10-digit integers as seconds and either convert to ms or reject with a clearer message) rather than hard-rejecting all ms < 1e12.
export function toEpochMs(value: string | number | undefined): number {
  const EPOCH_MS_MIN = 1e12
  if (value === undefined || value === null || value === '') {
    throw new PayloadValidationError('event_at is required')
  }
  if (typeof value === 'number' && Number.isInteger(value) && value >= EPOCH_MS_MIN) return value
  if (typeof value === 'string') {
    const trimmed = value.trim()
    const isDigitsOnly = /^\d+$/.test(trimmed)
    if (isDigitsOnly && Number(trimmed) >= EPOCH_MS_MIN) return Number(trimmed)
    if (!isDigitsOnly) {
      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)}`
  )
}

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • These casts force resolvedPayload / resolvedPayloads to StandardEvent even when the action is a CustomEvent. It works at runtime but undermines type-safety (e.g., could hide missing custom_event_name handling or allow incompatible payload shaping without compiler feedback). Suggested fix: keep resolvedPayload typed as Payload (or StandardEvent | CustomEvent) and pass it directly to send/sendV3, which already accept unions; avoid the as StandardEvent casts.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

      // api_version is a static per-mapping setting, not derived from event data, so a batch is
      // always homogeneously all-V2 or all-V3 - checking the first payload is enough.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:48

  • events[indices.indexOf(index)] is O(n) inside a loop, making this O(n²) across payloads, and it’s redundant because event is already in scope and corresponds to the item being recorded. Use sent: event (cast as needed) and drop indices/indexOf to reduce complexity and improve readability.
  const indices: number[] = []
  const events: EventItemV3[] = []

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • events[indices.indexOf(index)] is O(n) inside a loop, making this O(n²) across payloads, and it’s redundant because event is already in scope and corresponds to the item being recorded. Use sent: event (cast as needed) and drop indices/indexOf to reduce complexity and improve readability.
      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts:3

  • The previous file-level JSDoc (including the API reference/changelog link) was removed. Since this file defines the public version constants that drive endpoint selection, it would be helpful to restore a brief doc comment explaining what “legacy” vs “latest” mean and linking to the official Reddit API docs/changelog.
export const LEGACY_API_VERSION = 'v2.0'
export const LATEST_API_VERSION = 'v3'
export type ApiVersion = typeof LEGACY_API_VERSION | typeof LATEST_API_VERSION

packages/destination-actions/src/destinations/reddit-conversions-api/metadata.json:31

  • The description says “set the Test ID field on an action mapping”, but in this PR test_id is implemented as a destination-level setting (used as settings.test_id in v3 payload creation). Update this description to match the actual configuration surface (either “Test ID setting” or move test_id to an action-mapping field if that’s the intended design).
        "label": "[Deprecated] Test Mode",
        "description": "Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2, which is deprecated - V3 is the latest API version. To send test events on V3, set the Test ID field on an action mapping instead.",

Copilot AI review requested due to automatic review settings August 19, 2026 12:35
@joe-ayoub-segment
joe-ayoub-segment marked this pull request as ready for review August 19, 2026 12:43
@joe-ayoub-segment
joe-ayoub-segment requested a review from a team as a code owner August 19, 2026 12:43
@github-actions
github-actions Bot requested a review from harsh-joshi99 August 19, 2026 12:43
@joe-ayoub-segment joe-ayoub-segment self-assigned this Aug 19, 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 21 out of 21 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

packages/destination-actions/src/destinations/reddit-conversions-api/index.ts:36

  • The PR description states 'Settings-level test_mode field removed', but test_mode is still present in the destination settings (and referenced in metadata/generated types). Either remove test_mode from code + metadata as described, or update the PR description to reflect that it remains supported for v2.
      test_mode: {
        label: 'Test Mode',
        description:
          'Indicates if events should be treated as test events by Reddit. Only applies to Reddit Conversions API V2. V3 (Beta) is the latest API version. To send test events on V3, set the Test ID setting instead.',
        type: 'boolean',
        required: false,
        default: false
      },

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:62

  • These casts force all payloads to StandardEvent even when the action is customEvent. While it likely works at runtime due to structural typing, it weakens compile-time guarantees and can mask future regressions (e.g., if v2/v3 senders diverge further). Consider keeping the payload type as the generic Payload (or (StandardEvent | CustomEvent)) all the way through, and passing that through to send/sendV3 without narrowing via as StandardEvent.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:51

  • The indices array plus indices.indexOf(index) makes success recording O(n²) over the batch and is unnecessary because event is already available at the success site. You can set sent directly to event (or events[events.length - 1]) and drop indices entirely.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {
    try {

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • The indices array plus indices.indexOf(index) makes success recording O(n²) over the batch and is unnecessary because event is already available at the success site. You can set sent directly to event (or events[events.length - 1]) and drop indices entirely.
      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts:3

  • The previous file-level doc comment (including the Reddit API changelog reference) was removed. Since these constants define wire-level compatibility, consider reintroducing a brief comment and link (updated for v3) so future changes to version strings/endpoints are easier to validate.
export const LEGACY_API_VERSION = 'v2.0'
export const LATEST_API_VERSION = 'v3'
export type ApiVersion = typeof LEGACY_API_VERSION | typeof LATEST_API_VERSION

Copilot AI review requested due to automatic review settings August 19, 2026 12:52

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 21 out of 21 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:50

  • Building indices and then doing indices.indexOf(index) makes success recording O(n²) and is unnecessary because event is already in scope. Use event directly for sent (or use events[events.length - 1] immediately after pushing) and remove the indices array to simplify the logic and reduce overhead for large batches.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • Building indices and then doing indices.indexOf(index) makes success recording O(n²) and is unnecessary because event is already in scope. Use event directly for sent (or use events[events.length - 1] immediately after pushing) and remove the indices array to simplify the logic and reduce overhead for large batches.
      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:62

  • These casts force resolvedPayload/resolvedPayloads to StandardEvent even when the action is customEvent, which defeats type safety and can hide real schema/type issues during future changes. Keep the resolved payload typed as the generic Payload (or (StandardEvent | CustomEvent)) and pass that through to send/sendV3 without narrowing it to StandardEvent.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:111

  • You compute trimmed but parse the untrimmed original value with Date.parse(value). This can cause avoidable validation failures for ISO timestamps with leading/trailing whitespace. Parse trimmed instead to match the intent of the earlier normalization.
  if (typeof value === 'string') {
    const trimmed = value.trim()
    const isDigitsOnly = /^\d+$/.test(trimmed)
    if (isDigitsOnly && Number(trimmed) >= EPOCH_MS_MIN) return Number(trimmed)
    if (!isDigitsOnly) {
      const ms = Date.parse(value)
      if (!Number.isNaN(ms)) return ms
    }
  }

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:53

  • The PR title says 'add v3 support behind feature flag', but the implementation gates via a per-mapping API Version dropdown (no feature flag). To avoid confusion in release notes and reviews, update the PR title (or the referenced ticket scope) to reflect the actual rollout mechanism.
export const api_version: InputField = {
  label: 'API Version',
  description:
    'The version of the Reddit Conversions API to send this event to. "V3 (Beta)" requires Action Source to be set.',
  type: 'string',
  required: false,
  default: LEGACY_API_VERSION,
  choices: [
    { label: 'V3 (Beta)', value: LATEST_API_VERSION },
    { label: 'V2', value: LEGACY_API_VERSION }
  ],
  disabledInputMethods: ['literal', 'variable', 'function', 'freeform', 'enrichment']
}

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

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 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (7)

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:48

  • In createRedditPayloadV3, indices.indexOf(index) inside the loop makes payload construction O(n²) for batched requests. Since event is already available (and events are appended in the same iteration), you can remove indices entirely and set sent directly from event (or from events[events.length - 1]) to keep this O(n) and simplify control flow.
  const indices: number[] = []
  const events: EventItemV3[] = []

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:85

  • In createRedditPayloadV3, indices.indexOf(index) inside the loop makes payload construction O(n²) for batched requests. Since event is already available (and events are appended in the same iteration), you can remove indices entirely and set sent directly from event (or from events[events.length - 1]) to keep this O(n) and simplify control flow.
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:62

  • buildEventAction casts resolvedPayload / resolvedPayloads to StandardEvent even when building the customEvent action. This defeats type-safety and makes it easier to accidentally rely on StandardEvent-only fields later. Consider keeping resolvedPayload typed as Payload (the generic) and adjusting send to accept Payload[] (or overloading send / adding a generic) so both standard/custom actions remain strongly typed without assertions.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • buildEventAction casts resolvedPayload / resolvedPayloads to StandardEvent even when building the customEvent action. This defeats type-safety and makes it easier to accidentally rely on StandardEvent-only fields later. Consider keeping resolvedPayload typed as Payload (the generic) and adjusting send to accept Payload[] (or overloading send / adding a generic) so both standard/custom actions remain strongly typed without assertions.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:109

  • toEpochMs rejects epoch-millisecond timestamps earlier than 1e12 (Sep 2001). Epoch-ms values for legitimate historical events (e.g., 1990s backfills) are < 1e12 and will incorrectly throw. If the intent is only to reject epoch seconds, consider validating by digit-length/magnitude (e.g., treat ~1e9–1e10 as seconds) while allowing any non-negative epoch-ms value.
export function toEpochMs(value: string | number | undefined): number {
  const EPOCH_MS_MIN = 1e12

packages/destination-actions/src/destinations/reddit-conversions-api/standardEvent/generated-types.ts:37

  • These docstrings say quantity / item_price only apply to v3, but the PR description and the v2 implementation update in utils.ts indicate they are included in the v2 wire payload as well. To avoid misleading API consumers, update the generated-type comments (and corresponding field descriptions in fields.ts / metadata.json if needed) to reflect that these fields are sent for both v2 and v3 (or, alternatively, stop sending them in v2 if that’s not intended).
    /**
     * The number of this product in the event. Only applies to Reddit Conversions API V3 (Beta).
     */
    quantity?: number
    /**
     * The unit price of the product. Only applies to Reddit Conversions API V3 (Beta).
     */
    item_price?: number

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:26

  • PR title references 'behind feature flag', but the implementation (including resolveVersion) uses only the per-mapping api_version field for rollout and no flag gating. Consider updating the PR title (or adding a brief note in code comments/docs) so the reviewed change matches the stated rollout mechanism.
export function resolveVersion(apiVersion: string | undefined): ApiVersion {
  return apiVersion === LATEST_API_VERSION ? LATEST_API_VERSION : LEGACY_API_VERSION
}

Copilot AI review requested due to automatic review settings August 20, 2026 16:29

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 21 out of 21 changed files in this pull request and generated 1 comment.

Suppressed comments (7)

packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts:3

  • The PR title says “add v3 support behind feature flag”, but the implementation uses only the new per-mapping api_version field (no feature flag). Please update the PR title (or add the flag gating) so the PR description/title and rollout mechanism are consistent.
export const LEGACY_API_VERSION = 'v2.0'
export const LATEST_API_VERSION = 'v3'
export type ApiVersion = typeof LEGACY_API_VERSION | typeof LATEST_API_VERSION

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:345

  • The field descriptions say quantity / item_price “Only applies” to v3, but this PR also adds them to the v2 wire payload (utils.ts now sends them for v2 as well). Please update these descriptions (and the corresponding generated-types docs) to reflect that they’re sent for both API versions (or clarify the exact behavior).
    quantity: {
      label: 'Quantity',
      description: 'The number of this product in the event. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'integer',
      required: false
    },
    item_price: {
      label: 'Item Price',
      description: 'The unit price of the product. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'number',
      required: false
    }

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • The as StandardEvent casts make this factory type-unsafe for customEvent (those payloads are CustomEvent, not StandardEvent). This can hide real typing mistakes and makes future refactors riskier. Prefer keeping the resolved payload(s) typed as the generic Payload (or as (StandardEvent | CustomEvent)) and adjust send/sendV3 typing accordingly so the compiler enforces correctness without casts.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

      // api_version is a static per-mapping setting, not derived from event data, so a batch is
      // always homogeneously all-V2 or all-V3 - checking the first payload is enough.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:51

  • The indices + indices.indexOf(index) lookup is unnecessary and adds avoidable O(n) work per item. Since event is already available in-scope, you can set sent: event directly (and remove indices entirely), which is simpler and avoids quadratic behavior for large batches.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {
    try {

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • The indices + indices.indexOf(index) lookup is unnecessary and adds avoidable O(n) work per item. Since event is already available in-scope, you can set sent: event directly (and remove indices entirely), which is simpler and avoids quadratic behavior for large batches.
      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:38

  • For batch sends, createRedditPayloadV3 pre-populates per-item successes with status: 200 before the HTTP call, and sendV3 always returns that MultiStatusResponse regardless of the actual HTTP status unless the request client throws. If the request client returns a non-2xx response without throwing, this will incorrectly report successes. Consider explicitly checking the HTTP response status (and throwing/failing the batch) to ensure the MultiStatusResponse can’t mask an upstream failure.
  if (data.data.events.length) {
    const response = await request(
      `https://ads-api.reddit.com/api/${LATEST_API_VERSION}/pixels/${settings.ad_account_id}/conversion_events`,
      {
        method: 'POST',
        headers: { Authorization: `Bearer ${settings.conversion_token}` },
        json: JSON.parse(JSON.stringify(data))
      }
    )
    if (!isBatch) {
      return response
    }
  }

  return multiStatusResponse

packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts:3

  • The previous JSDoc explaining the constant and linking the API reference was removed. Since these version constants drive routing and wire endpoints, consider re-adding brief JSDoc (including the API reference link) so future maintainers have context for what these versions represent and where to validate changes.
export const LEGACY_API_VERSION = 'v2.0'
export const LATEST_API_VERSION = 'v3'
export type ApiVersion = typeof LEGACY_API_VERSION | typeof LATEST_API_VERSION

Copilot AI review requested due to automatic review settings August 20, 2026 16:47
@joe-ayoub-segment joe-ayoub-segment changed the title STRATCONN-6892 - [Reddit Conversions API] - add v3 support behind feature flag STRATCONN-6892 - [Reddit Conversions API] - add v3 support Aug 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 21 out of 21 changed files in this pull request and generated no new comments.

Suppressed comments (5)

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • The as StandardEvent / as StandardEvent[] casts defeat the generic typing of buildEventAction and can mask type errors for customEvent (where the payload is not a StandardEvent). Prefer keeping resolvedPayload/resolvedPayloads typed as the generic Payload (or StandardEvent | CustomEvent) and pass those through, so TypeScript continues enforcing the correct shape per action.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

      // api_version is a static per-mapping setting, not derived from event data, so a batch is
      // always homogeneously all-V2 or all-V3 - checking the first payload is enough.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • indices.indexOf(index) inside the loop adds unnecessary work (O(n) per event), and the indices array is redundant since event is already available. Use the local event value (or events[events.length - 1]) when setting sent and remove indices entirely to keep payload building O(n) and simpler to maintain.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {
    try {
      const {
        event_at,
        click_id,
        products,
        user,
        data_processing_options,
        screen_dimensions,
        event_metadata,
        conversion_id,
        action_source,
        event_source_url
      } = payload

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

      const event: EventItemV3 = {
        event_at: toEpochMs(event_at),
        action_source: toActionSourceV3(action_source),
        event_source_url: clean(event_source_url),
        click_id: clean(click_id),
        type: {
          tracking_type: toV3TrackingType(tracking_type),
          custom_event_name
        },
        metadata: getMetadata(event_metadata, products, conversion_id),
        user: getUser(user, data_processing_options, screen_dimensions)
      }

      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:111

  • Date.parse(value) should use the already-trimmed string (trimmed) for consistency with the digits-only branch and to avoid surprising failures on inputs with leading/trailing whitespace.
  if (typeof value === 'string') {
    const trimmed = value.trim()
    const isDigitsOnly = /^\d+$/.test(trimmed)
    if (isDigitsOnly && Number(trimmed) >= EPOCH_MS_MIN) return Number(trimmed)
    if (!isDigitsOnly) {
      const ms = Date.parse(value)
      if (!Number.isNaN(ms)) return ms
    }
  }

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:345

  • These descriptions say quantity/item_price are v3-only, but this PR also sends them on the wire for v2 (see v2 getProducts in utils.ts). Update the field descriptions (and the corresponding generated-types/metadata descriptions) to reflect that these fields apply to both v2 and v3 to avoid misleading customers.
    quantity: {
      label: 'Quantity',
      description: 'The number of this product in the event. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'integer',
      required: false
    },
    item_price: {
      label: 'Item Price',
      description: 'The unit price of the product. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'number',
      required: false
    }

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:53

  • The PR title indicates “behind feature flag”, but the implementation (and PR description) uses the per-mapping API Version field as the rollout mechanism with no flag. Consider updating the PR title (or other visible release notes) to match the actual rollout approach so reviewers/operators aren’t expecting a flag-gated deploy.
export const api_version: InputField = {
  label: 'API Version',
  description:
    'The version of the Reddit Conversions API to send this event to. "V3 (Beta)" requires Action Source to be set.',
  type: 'string',
  required: false,
  default: LEGACY_API_VERSION,
  choices: [
    { label: 'V3 (Beta)', value: LATEST_API_VERSION },
    { label: 'V2', value: LEGACY_API_VERSION }
  ],
  disabledInputMethods: ['literal', 'variable', 'function', 'freeform', 'enrichment']
}

…I docs

PageVisit/ViewContent/Search do not support currency/value/item_count at
all; Lead/SignUp support currency/value but not item_count. Reddit staging
diagnostics flagged both cases ("unsupported metadata" for Search/View
Content). Adds supportsValueMetadata/supportsItemCount helpers shared by
both the V2 and V3 wire-payload builders, plus unit tests for each.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 21, 2026 11:06

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 22 out of 22 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • The as StandardEvent / as StandardEvent[] casts erase type-safety for the customEvent action path (where payloads are CustomEvent). This can mask real shape mismatches at compile time. Prefer keeping resolvedPayload typed as Payload (or StandardEvent | CustomEvent) and adjusting send()/sendV3() signatures (e.g., overloads or a shared union-array type) so no unsafe cast is needed.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

      // api_version is a static per-mapping setting, not derived from event data, so a batch is
      // always homogeneously all-V2 or all-V3 - checking the first payload is enough.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • The indices array + indices.indexOf(index) lookup is unnecessary and makes this loop O(n²) in the success path. Since event is already in scope (and just pushed), you can set sent to event (or events[events.length - 1]) and drop indices entirely.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {
    try {
      const {
        event_at,
        click_id,
        products,
        user,
        data_processing_options,
        screen_dimensions,
        event_metadata,
        conversion_id,
        action_source,
        event_source_url
      } = payload

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

      const event: EventItemV3 = {
        event_at: toEpochMs(event_at),
        action_source: toActionSourceV3(action_source),
        event_source_url: clean(event_source_url),
        click_id: clean(click_id),
        type: {
          tracking_type: toV3TrackingType(tracking_type),
          custom_event_name
        },
        metadata: getMetadata(event_metadata, products, conversion_id, tracking_type),
        user: getUser(user, data_processing_options, screen_dimensions)
      }

      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:345

  • These field descriptions state quantity / item_price only apply to v3, but the PR description says they are sent on the wire for both v2 and v3, and v2 payload-building now includes them. Please update these descriptions (and regenerate metadata.json / generated-types comments if needed) so customers aren’t misled about when the fields take effect.
    quantity: {
      label: 'Quantity',
      description: 'The number of this product in the event. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'integer',
      required: false
    },
    item_price: {
      label: 'Item Price',
      description: 'The unit price of the product. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'number',
      required: false
    }

packages/destination-actions/src/destinations/reddit-conversions-api/utils.ts:96

  • This introduces new v2 behavior: v2 payloads will now drop currency/value_decimal/item_count for certain tracking_type values via the new supportsValueMetadata / supportsItemCount gating in getMetadata(). The PR description says v2 is 'completely untouched' except for adding product fields; either update the PR description to include this v2 behavior change, or confine this metadata filtering to v3 only (keeping v2 output unchanged).
// Per https://business.reddithelp.com/s/article/about-event-metadata: PageVisit/ViewContent/Search
// don't support currency/value/item_count at all (conversion_id/products are still fine), and
// Lead/SignUp support currency/value but not item_count.
const TRACKING_TYPES_WITHOUT_VALUE_METADATA = new Set(['PageVisit', 'ViewContent', 'Search'])
const TRACKING_TYPES_WITHOUT_ITEM_COUNT = new Set(['Lead', 'SignUp'])

export function supportsValueMetadata(trackingType: string | undefined): boolean {
  return !TRACKING_TYPES_WITHOUT_VALUE_METADATA.has(trackingType ?? '')
}

export function supportsItemCount(trackingType: string | undefined): boolean {
  return supportsValueMetadata(trackingType) && !TRACKING_TYPES_WITHOUT_ITEM_COUNT.has(trackingType ?? '')
}

Copilot AI review requested due to automatic review settings August 21, 2026 11:24

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 22 out of 22 changed files in this pull request and generated no new comments.

Suppressed comments (4)

packages/destination-actions/src/destinations/reddit-conversions-api/action.ts:69

  • The as StandardEvent / as StandardEvent[] casts are incorrect for the customEvent action path and weaken type safety (they can mask mismatches between v2/v3 payload expectations). Prefer keeping resolvedPayload/resolvedPayloads typed as Payload (or StandardEvent | CustomEvent) and adjust send() typing to accept (StandardEvent | CustomEvent)[] (or add overloads) so no unsafe casts are needed.
    perform: async (request, { settings, payload }) => {
      const resolvedPayload = resolvePayload(payload) as StandardEvent
      return resolveVersion(payload.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, [resolvedPayload], false)
        : send(request, settings, [resolvedPayload])
    },
    performBatch: async (request, { settings, payload }) => {
      const resolvedPayloads = payload.map(resolvePayload) as StandardEvent[]

      // api_version is a static per-mapping setting, not derived from event data, so a batch is
      // always homogeneously all-V2 or all-V3 - checking the first payload is enough.
      return resolveVersion(resolvedPayloads[0]?.api_version) === LATEST_API_VERSION
        ? sendV3(request, settings, resolvedPayloads, true)
        : send(request, settings, resolvedPayloads)
    }

packages/destination-actions/src/destinations/reddit-conversions-api/v3/utils-v3.ts:87

  • The indices array + indices.indexOf(index) lookup is redundant and adds an O(n) search per event. You can set sent directly from the local event (or use events[events.length - 1]) and remove indices entirely, which simplifies the logic and avoids extra work in large batches.
  const indices: number[] = []
  const events: EventItemV3[] = []

  payloads.forEach((payload, index) => {
    try {
      const {
        event_at,
        click_id,
        products,
        user,
        data_processing_options,
        screen_dimensions,
        event_metadata,
        conversion_id,
        action_source,
        event_source_url
      } = payload

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

      const event: EventItemV3 = {
        event_at: toEpochMs(event_at),
        action_source: toActionSourceV3(action_source),
        event_source_url: clean(event_source_url),
        click_id: clean(click_id),
        type: {
          tracking_type: toV3TrackingType(tracking_type),
          custom_event_name
        },
        metadata: getMetadata(event_metadata, products, conversion_id, tracking_type),
        user: getUser(user, data_processing_options, screen_dimensions)
      }

      indices.push(index)
      events.push(event)
      multiStatusResponse.setSuccessResponseAtIndex(index, {
        status: 200,
        sent: events[indices.indexOf(index)] as unknown as JSONLikeObject,
        body: { success: true }
      })

packages/destination-actions/src/destinations/reddit-conversions-api/fields.ts:345

  • These descriptions say quantity/item_price "only apply" to v3, but the PR description states these fields are intentionally sent on the wire for both v2 and v3. Please update the field descriptions (and the generated-types JSDoc / metadata.json text that mirrors them) to reflect actual behavior (e.g., "Sent for both v2 and v3"), or alternatively gate the v2 payload so the statement remains accurate.
    quantity: {
      label: 'Quantity',
      description: 'The number of this product in the event. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'integer',
      required: false
    },
    item_price: {
      label: 'Item Price',
      description: 'The unit price of the product. Only applies to Reddit Conversions API V3 (Beta).',
      type: 'number',
      required: false

packages/destination-actions/src/destinations/reddit-conversions-api/versioning-info.ts:3

  • The previous exported version constant included an API reference comment; with the rename to LEGACY/LATEST, that context is lost. Consider reintroducing a brief doc comment explaining what these constants correspond to (and linking the relevant Reddit API docs/changelog), especially since 'LATEST' currently refers to a beta choice in the UI.
export const LEGACY_API_VERSION = 'v2.0'
export const LATEST_API_VERSION = 'v3'
export type ApiVersion = typeof LEGACY_API_VERSION | typeof LATEST_API_VERSION

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.

2 participants