Skip to content

[Vibe] - new Conversions destination#3854

Open
mdkhan-tw wants to merge 2 commits into
mainfrom
mz-vibe-conversions
Open

[Vibe] - new Conversions destination#3854
mdkhan-tw wants to merge 2 commits into
mainfrom
mz-vibe-conversions

Conversation

@mdkhan-tw

Copy link
Copy Markdown
Contributor

Adds a new cloud-mode destination, Vibe Tracking Event (slug actions-vibe-conversions), that sends conversion events to Vibe via POST https://t.vibe.co/s2s-conversion/events/segment.

What & why: Vibe wants to receive Segment conversion events (purchases, page views, signups, etc.) for advertiser attribution. This destination maps Segment track/page events to Vibe's single-event conversion API.

Details

  • Action: trackConversion — 9 mapped fields (a, eid, ts, ip, em, ed, gid, ua, url) plus the aid (Pixel ID) settings value injected into every event.
  • Auth: scheme: custom. The Vibe conversion API requires no API key/token; the advertiser is identified by the Pixel ID (aid) setting.
  • Presets: Order Completed → purchase, Page Viewed → page_view, Signed Up → signup.
  • Validation & transforms:
    • Either ip or em is required (throws PayloadValidationError if both missing).
    • Timestamp must be within the last 7 days; ISO timestamps are converted to UNIX milliseconds.
    • Event type a is constrained to the API enum via choices.
    • Event data (ed) is serialized to a stringified JSON object.
  • No batching: the Vibe API has no batch endpoint, so performBatch is intentionally omitted (single-event POST only).

TODO before merge

  • ⚠️ Not registered in packages/destination-actions/src/destinations/index.ts yet — the metadata ID must be the production-assigned MongoDB ObjectId created in the control plane, not a placeholder. Register once the destination is created in production.
  • Open questions to confirm with Vibe: endpoint path /events/segment (PRD) vs /events (public docs); em field and call enum value present in PRD but absent from public docs; final destination name (currently "Vibe Tracking Event", TBC); gid label/description.

Testing

  • Added unit tests for new functionality
  • Tested end-to-end using the local server
  • [If destination is already live] Tested for backward compatibility of destination.
  • [Segmenters] Tested in the staging environment
  • [Segmenters] [If applicable for this change] Tested for regression with Hadron.

11 Jest tests pass (unit + snapshot), covering the happy path, ISO→UNIX ms conversion, email fallback, both validation errors, enum rejection, presets, and request-body snapshots. Typecheck and lint are clean.

Security Review

  • Reviewed all field definitions for sensitive data (API keys, tokens, passwords, client secrets). This API uses no secrets; the only credential-like value is the non-secret Pixel ID.

New Destination Checklist

  • Extracted all action API versions to versioning-info.ts file.

🤖 Generated with Claude Code

Add a new cloud-mode destination that sends conversion events to Vibe
via POST https://t.vibe.co/s2s-conversion/events/segment.

- Single action `trackConversion` with 9 mapped fields plus the `aid`
  (Pixel ID) settings value injected into every event.
- 3 presets: Order Completed -> purchase, Page Viewed -> page_view,
  Signed Up -> signup.
- Validation: either/or ip/em requirement, 7-day timestamp window, and
  enum-constrained event type; transforms ISO timestamps to UNIX ms and
  serializes event data to a stringified JSON object.
- No batch endpoint on the Vibe API, so performBatch is intentionally
  omitted (single-event POST only).

Not registered in destinations/index.ts yet: the metadata ID must be the
production-assigned ObjectId created in the control plane.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mdkhan-tw mdkhan-tw requested a review from a team as a code owner July 3, 2026 10:24
Copilot AI review requested due to automatic review settings July 3, 2026 10: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

Note

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

Adds a new Segment Actions (cloud) destination “Vibe Tracking Event” to POST single conversion events to Vibe’s S2S conversion endpoint.

Changes:

  • Introduces the trackConversion action with field mappings, presets, and request construction/sending helpers.
  • Adds validation/transforms for timestamp and identity requirements (IP/email), plus request-body serialization.
  • Adds Jest unit + snapshot tests for the new destination/action.

Reviewed changes

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

Show a summary per file
File Description
packages/destination-actions/src/destinations/vibe-conversions/constants.ts Defines Vibe base URL, endpoint, allowed event types, and max event age.
packages/destination-actions/src/destinations/vibe-conversions/generated-types.ts Adds generated Settings type (aid).
packages/destination-actions/src/destinations/vibe-conversions/index.ts Declares the destination, authentication, action registration, and presets.
packages/destination-actions/src/destinations/vibe-conversions/metadata.json Adds destination/action metadata for control-plane compatibility.
packages/destination-actions/src/destinations/vibe-conversions/types.ts Defines request/response shapes and the EventType union.
packages/destination-actions/src/destinations/vibe-conversions/utils.ts Implements payload transforms/validation and the POST request sender.
packages/destination-actions/src/destinations/vibe-conversions/tests/index.test.ts Tests destination auth + preset exposure.
packages/destination-actions/src/destinations/vibe-conversions/tests/snapshot.test.ts Snapshot tests request body/headers with deterministic time.
packages/destination-actions/src/destinations/vibe-conversions/tests/snapshots/snapshot.test.ts.snap Snapshot artifacts for request body/headers.
packages/destination-actions/src/destinations/vibe-conversions/trackConversion/index.ts Defines the trackConversion action fields and perform.
packages/destination-actions/src/destinations/vibe-conversions/trackConversion/generated-types.ts Adds generated Payload type for the action.
packages/destination-actions/src/destinations/vibe-conversions/trackConversion/tests/index.test.ts Unit tests covering mapping, validation, enums, and timestamp conversion.

Comment on lines +34 to +39
if (tsMs !== undefined) {
const now = Date.now()
if (now - tsMs > MAX_EVENT_AGE_MS) {
throw new PayloadValidationError('`ts` (timestamp) must be within the last 7 days.')
}
}
Comment on lines +29 to +31
if (!ip && !em) {
throw new PayloadValidationError('Either `ip` (IP Address) or `em` (Email) is required.')
}
eid,
aid: settings.aid,
ts: tsMs,
ip: ip || undefined,
}

return {
a: a as EventType,
// Convert an ISO8601 / epoch timestamp to UNIX milliseconds.
function toUnixMs(ts: Payload['ts']): number | undefined {
if (ts === undefined || ts === null || ts === '') return undefined
const ms = typeof ts === 'number' ? ts : new Date(ts).getTime()
…ields

- Add `performBatch` with per-event error isolation via MultiStatusResponse.
  Vibe has no batch endpoint, so each event is still sent as its own POST;
  batching lets Segment group events and isolates invalid/failed events so
  one bad event does not fail the whole batch. Adds enable_batching (default
  true) and hidden batch_size fields.
- Add dedicated `price_usd` (number) and `purchase_id` (string) fields, the
  reserved Vibe event-data attributes, defaulted from common ecommerce
  properties and merged into the stringified `ed` object.
- `ed` is now omitted entirely when there is no event data (previously sent
  an empty "{}").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@joe-ayoub-segment joe-ayoub-segment changed the title [Vibe Tracking Event] - add conversion events destination [Vibe] - new Conversions destination Jul 3, 2026
Comment on lines +18 to +19
// Vibe's conversion API requires no API key or token. The advertiser is
// identified by the Pixel ID (aid), which is injected into every event.

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.

let's remove unnecessary comments

Comment on lines +7 to +8
// Preset mappings reuse the action's default field values, but pin `a` to a
// preset-specific enum value (the `a` field itself intentionally has no default).

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.

remove comments.

ed: {
label: 'Event Data',
description:
'Event data. Sent to Vibe as a stringified JSON object. The reserved attributes Price (USD) and Purchase ID are merged into this object.',

@joe-ayoub-segment joe-ayoub-segment Jul 3, 2026

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.

Suggested change
'Event data. Sent to Vibe as a stringified JSON object. The reserved attributes Price (USD) and Purchase ID are merged into this object.',
'Additional event data to send with the event.',

Comment on lines +64 to +91
price_usd: {
label: 'Price (USD)',
description:
'Reserved event-data attribute: the price of the conversion in USD. Merged into Event Data as `price_usd`.',
type: 'number',
required: false,
default: {
'@if': {
exists: { '@path': '$.properties.price_usd' },
then: { '@path': '$.properties.price_usd' },
else: { '@path': '$.properties.price' }
}
}
},
purchase_id: {
label: 'Purchase ID',
description:
'Reserved event-data attribute: a unique identifier for the purchase. Merged into Event Data as `purchase_id`.',
type: 'string',
required: false,
default: {
'@if': {
exists: { '@path': '$.properties.purchase_id' },
then: { '@path': '$.properties.purchase_id' },
else: { '@path': '$.properties.order_id' }
}
}
},

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.

Personally I think these fields should be included as properties in the ed field.

for example something like this:

ed: {
label: 'Event Data',
description: 'Additional data to be sent with the event.',
type: 'object',
additionalProperties: true,
properties: {
purchase_id: {
label: 'Purchase ID',
description:'Reserved event-data attribute: a unique identifier for the purchase. Merged into Event Data as purchase_id.',
type: 'string'
},
price_usd: {
label: 'Price (USD)',
description: 'Reserved event-data attribute: the price of the conversion in USD. Merged into Event Data as price_usd.',
type: 'number'
}
},
default: {
purchase_id: {'@path': '$.properties.purchase_id'},
price_usd: { '@path': '$.properties.price_usd' }
}
},

}
},
perform: (request, { payload, settings }) => {
return sendEvent(request, settings, payload)

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.

Usually I prefer to have a single shared code path for single events and batch events. The path needs accepts an array of payloads, so we wrap the single payload in an array.

We then need code branches inside the shared function to handle multistatus responses when dealing with a real batch.

The last param I added (false) is an isBatch param which can be used in the sendBatch function.

This is a preference I have - but others may disagree with it.

Suggested change
return sendEvent(request, settings, payload)
return sendBatch(request, settings, [payload], false)

Comment on lines +17 to +18
if (Object.keys(merged).length === 0) return undefined
return JSON.stringify(merged)

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.

Suggested change
if (Object.keys(merged).length === 0) return undefined
return JSON.stringify(merged)
if (Object.keys(merged).length === 0) {
return undefined
}
return JSON.stringify(merged)

): Promise<MultiStatusResponse> {
const multiStatusResponse = new MultiStatusResponse()

await Promise.all(

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.

This is a bad idea. We should only implement performBatch if the destination API actually has a dedicated batch endpoint to hit. In this case all we're doing is sending a load of single events using Promise.all. Imagine if we get a batch of 1000 events, we'll end up sending 1000 separate post requests :(.

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.

3 participants