STRATCONN-6824 - [Braze] - ecommerce.cart_updated support - #3804
STRATCONN-6824 - [Braze] - ecommerce.cart_updated support#3804joe-ayoub-segment wants to merge 7 commits into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR attempts to add Braze ecommerce support for an additional event type by introducing ecommerce.cart_updated alongside existing multi-product ecommerce recommended events.
Changes:
- Enabled the
CART_UPDATEDevent name constant and added it into the ecommerce event unions/types. - Added
CART_UPDATEDhandling in the ecommerce JSON builder (getJSONItem) to emit a Cart Updated event payload. - Expanded several ecommerce event TypeScript interfaces with additional optional pricing fields (e.g.,
subtotal_value,tax,shipping).
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| packages/destination-actions/src/destinations/braze/ecommerce/types.ts | Adds CartUpdatedEvent and extends ecommerce event typings/unions. |
| packages/destination-actions/src/destinations/braze/ecommerce/functions.ts | Adds runtime JSON-building support for EVENT_NAMES.CART_UPDATED. |
| packages/destination-actions/src/destinations/braze/ecommerce/constants.ts | Enables CART_UPDATED in the Braze ecommerce event name constants. |
Comments suppressed due to low confidence (2)
packages/destination-actions/src/destinations/braze/ecommerce/constants.ts:6
CART_UPDATEDis now defined/handled in code, but the ecommerce action’s field definitions still have the “Cart Updated” choice and relateddepends_on/requiredconditions commented out (seepackages/destination-actions/src/destinations/braze/ecommerce/fields.ts). As-is, users likely can’t select/configure this event through the action schema, so the added support won’t be reachable. Please enable the choice and any required/depends_on rules and regenerate the action types as needed.
PRODUCT_VIEWED: 'ecommerce.product_viewed',
CHECKOUT_STARTED: 'ecommerce.checkout_started',
CART_UPDATED: 'ecommerce.cart_updated',
ORDER_PLACED: 'ecommerce.order_placed',
ORDER_CANCELLED: 'ecommerce.order_cancelled',
packages/destination-actions/src/destinations/braze/ecommerce/types.ts:92
CartUpdatedEvent.properties.cart_idis typed as required, but the actionPayloaddefinescart_idas optional and the field description suggests Braze can default it when omitted. Consider makingcart_idoptional inCartUpdatedEvent(and/or conditionally including it in the JSON properties object) to keep the TypeScript types aligned with the actual payload behavior.
export interface CartUpdatedEvent extends MultiProductBaseEvent {
name: CartUpdatedEventName
properties: MultiProductBaseEvent['properties'] & {
cart_id: string
action?: 'add' | 'remove' | 'replace'
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (2)
packages/destination-actions/src/destinations/braze/ecommerce/functions.ts:208
- In the
CART_UPDATEDbranch,cart_idis always added topropertiesvia a type assertion (cart_id: cart_id as string). If the input omitscart_id(currently allowed by the field schema), the outbound payload will includecart_id: undefined, which is likely to be rejected by Braze or cause inconsistent behavior. Prefer conditionally addingcart_idonly when present, or enforcecart_idas required for this event.
case EVENT_NAMES.CART_UPDATED: {
const { cart_id, action, subtotal_value, tax, shipping } = payload as Payload
const event: CartUpdatedEvent = {
...multiProductEvent,
name: EVENT_NAMES.CART_UPDATED,
properties: {
...multiProductEvent.properties,
cart_id: cart_id as string,
...(action ? { action: action as 'add' | 'remove' | 'replace' } : {}),
...(typeof subtotal_value === 'number' ? { subtotal_value } : {}),
...(typeof tax === 'number' ? { tax } : {}),
...(typeof shipping === 'number' ? { shipping } : {})
}
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:223
cart_idis included forCart Updatedviadepends_ononly, so it’s optional at validation time. Given the newCART_UPDATEDimplementation currently assumes a stringcart_id, either add arequiredcondition forname = ecommerce.cart_updated(and/or align the runtime to omit it when absent).
const cart_id: InputField = {
label: 'Cart ID',
description: 'Unique identifier for the cart. If no value is passed, Braze will determine a default value (shared across cart, checkout, and order events) for the user cart mapping.',
type: 'string',
default: {'@path': '$.properties.cart_id'},
depends_on: {
match: 'any',
conditions: [
{
fieldKey: 'name',
operator: 'is',
value: EVENT_NAMES.CART_UPDATED
},
{
fieldKey: 'name',
operator: 'is',
value: EVENT_NAMES.CHECKOUT_STARTED
},
{
fieldKey: 'name',
operator: 'is',
value: EVENT_NAMES.ORDER_PLACED
}
]
}
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:16
commonFields.nameis reused by both the multi-product and single-product actions, but the choice list now includesCart Updated. The single-product action doesn’t define aproductsfield, andsend()will treatecommerce.cart_updatedas a multi-product event and callpayload.products.map(...), which will throw at runtime if a user selects this option in the single-product action. Consider scopingnamechoices per action (e.g., overridenameinecommerceSingleProductto only allowPRODUCT_VIEWED) or add the requiredproductsfield/payload shape to the single-product action if it should support cart updates.
required: true,
choices: [
{ label: 'Product Viewed', value: EVENT_NAMES.PRODUCT_VIEWED },
{ label: 'Cart Updated', value: EVENT_NAMES.CART_UPDATED },
{ label: 'Checkout Started', value: EVENT_NAMES.CHECKOUT_STARTED },
{ label: 'Order Placed', value: EVENT_NAMES.ORDER_PLACED },
{ label: 'Order Cancelled', value: EVENT_NAMES.ORDER_CANCELLED },
{ label: 'Order Refunded', value: EVENT_NAMES.ORDER_REFUNDED }
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:16
commonFields.nameis shared by both the multi-productecommerceaction and the single-productecommerceSingleProductaction (seeecommerceSingleProduct/index.tsimportingcommonFields). EnablingCART_UPDATEDhere makes it selectable in the single-product action as well, butsend()handlesecommerce.cart_updatedas a multi-product event and will attempt to readpayload.products(which the single-product payload does not have), leading to a runtime error if a user selects this event name in the single-product action. Consider scoping thenamefield choices per action (e.g., a separatenamefield for single-product that only allowsPRODUCT_VIEWED), or updatesend()to safely handle single-product payloads forCART_UPDATED(and other multi-product events) without accessing missing fields.
required: true,
choices: [
{ label: 'Product Viewed', value: EVENT_NAMES.PRODUCT_VIEWED },
{ label: 'Cart Updated', value: EVENT_NAMES.CART_UPDATED },
{ label: 'Checkout Started', value: EVENT_NAMES.CHECKOUT_STARTED },
{ label: 'Order Placed', value: EVENT_NAMES.ORDER_PLACED },
{ label: 'Order Cancelled', value: EVENT_NAMES.ORDER_CANCELLED },
{ label: 'Order Refunded', value: EVENT_NAMES.ORDER_REFUNDED }
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.
Suppressed comments (5)
packages/destination-actions/src/destinations/braze/ecommerce/functions.ts:173
catalog_typeis treated as an array (catalog_type.length) and then passed through asproperties.type. If mappings provide a string (or other non-array), this will either send the wrong type to Braze (string instead of string[]) or behave unexpectedly. Consider guarding withArray.isArray(catalog_type)before checking length and including it, and optionally normalizing/filtering to the allowed values.
product,
catalog_type
} = payload as SingleProductPayload
const event: ProductViewedEvent = {
...baseEvent,
name: EVENT_NAMES.PRODUCT_VIEWED,
properties: {
...baseEvent.properties,
...product,
...(catalog_type && catalog_type.length > 0 ? { type: catalog_type } : {})
}
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:662
- Defaulting
catalog_typefrom$.properties.typeis risky becauseproperties.typeis a very common/overloaded attribute in ecommerce payloads (often meaning product type/category), and this could unintentionally start sending Braze catalog triggertypedata for existing Product Viewed events. Consider removing the default (leave unmapped by default), or sourcing from a less ambiguous property (e.g.,$.properties.catalog_type) so the feature is opt-in and doesn’t conflict with existing schemas.
const catalog_type: InputField = {
label: 'Catalog Trigger Type',
description: 'Required to use Braze catalog trigger features. Accepted values: price_drop, back_in_stock.',
type: 'string',
multiple: true,
choices: [
{ label: 'Price Drop', value: 'price_drop' },
{ label: 'Back In Stock', value: 'back_in_stock' }
],
default: { '@path': '$.properties.type' },
required: false,
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:253
- The PR description/staging plan indicates cart_updated may be sent with only
cart_id + products(omitting totals). This change makestotal_valuerequired forCART_UPDATEDin the UI. Either update the PR description/test plan to reflect thattotal_valueis required forcart_updated, or relax this requirement if Braze allows cart_updated withouttotal_value.
required: {
match: 'any',
conditions: [
{
fieldKey: 'name',
operator: 'is',
value: EVENT_NAMES.CART_UPDATED
},
{
fieldKey: 'name',
operator: 'is',
value: EVENT_NAMES.CHECKOUT_STARTED
},
packages/destination-actions/src/destinations/braze/ecommerce/functions.ts:210
actionis cast to'add' | 'remove' | 'replace'without runtime validation. If a mapping provides any other string, the payload will still include it and may be rejected by Braze. Consider validatingactionagainst the allowed set before including it (or emitting a clear error) rather than relying on a type cast.
const { cart_id, action, subtotal_value, tax, shipping } = payload as Payload
const event: CartUpdatedEvent = {
...multiProductEvent,
name: EVENT_NAMES.CART_UPDATED,
properties: {
...multiProductEvent.properties,
cart_id: cart_id as string,
...(action ? { action: action as 'add' | 'remove' | 'replace' } : {}),
...(typeof subtotal_value === 'number' ? { subtotal_value } : {}),
...(typeof tax === 'number' ? { tax } : {}),
...(typeof shipping === 'number' ? { shipping } : {})
}
packages/destination-actions/src/destinations/braze/ecommerce/types.ts:97
- There’s trailing whitespace on the
tax?: numberline. This can cause avoidable lint/prettier noise in future diffs; consider trimming it.
action?: 'add' | 'remove' | 'replace'
subtotal_value?: number
tax?: number
shipping?: number
Adding support to Braze destination for Ecommerce ecommerce.cart_updated events.
https://twilio-engineering.atlassian.net/browse/STRATCONN-6824
Also adds new presets for Product Added and Product Removed.
Testing
New unit tests:
Updated unit tests:
Staging Test Plan
subtotal_value, tax, shipping, and products. Verify Braze receives all fields.
subtotal/tax/shipping). Verify optional fields are omitted cleanly.
populated. Verify they appear in Braze payload.
'back_in_stock']. Verify Braze receives type: ["price_drop", "back_in_stock"] in properties.
events process correctly in a single request.
Security Review
Please ensure sensitive data is properly protected in your integration.
type: 'password'New Destination Checklist
verioning-info.tsfile. example