[Vibe] - new Conversions destination#3854
Conversation
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>
There was a problem hiding this comment.
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
trackConversionaction 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. |
| 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.') | ||
| } | ||
| } |
| 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>
| // 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. |
There was a problem hiding this comment.
let's remove unnecessary comments
| // 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). |
| 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.', |
There was a problem hiding this comment.
| '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.', |
| 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' } | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| return sendEvent(request, settings, payload) | |
| return sendBatch(request, settings, [payload], false) |
| if (Object.keys(merged).length === 0) return undefined | ||
| return JSON.stringify(merged) |
There was a problem hiding this comment.
| 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( |
There was a problem hiding this comment.
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 :(.
Adds a new cloud-mode destination, Vibe Tracking Event (slug
actions-vibe-conversions), that sends conversion events to Vibe viaPOST 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
trackConversion— 9 mapped fields (a,eid,ts,ip,em,ed,gid,ua,url) plus theaid(Pixel ID) settings value injected into every event.scheme: custom. The Vibe conversion API requires no API key/token; the advertiser is identified by the Pixel ID (aid) setting.purchase, Page Viewed →page_view, Signed Up →signup.iporemis required (throwsPayloadValidationErrorif both missing).ais constrained to the API enum viachoices.ed) is serialized to a stringified JSON object.performBatchis intentionally omitted (single-event POST only).TODO before merge
packages/destination-actions/src/destinations/index.tsyet — 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./events/segment(PRD) vs/events(public docs);emfield andcallenum value present in PRD but absent from public docs; final destination name (currently "Vibe Tracking Event", TBC);gidlabel/description.Testing
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
New Destination Checklist
versioning-info.tsfile.🤖 Generated with Claude Code