STRATCONN-6824 - [Braze] - ecommerce.cart_updated support - #3804
STRATCONN-6824 - [Braze] - ecommerce.cart_updated support#3804joe-ayoub-segment wants to merge 10 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
New required fields detectedWarning 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:
Add these new fields as optional instead and assume default values in |
…merge The merge of main into this branch added subtotal_value/tax/shipping to the shared test payload+mapping but left the checkout_started expected event in the 'no SyncMode' multistatus test without them, causing a Nock no-match failure. Add the fields to that fixture and regenerate the ecommerce snapshots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:1
- The PR description/test plan indicates a “cart_updated with minimal fields (just cart_id)” scenario, but
total_valueis now required forCART_UPDATED. Either (a) update the PR description/test plan to reflect thattotal_valueis required, or (b) removeEVENT_NAMES.CART_UPDATEDfrom thetotal_value.requiredconditions (and ensure the runtime payload builder + tests reflect that omission is supported).
import { InputField } from '@segment/actions-core'
packages/destination-actions/src/destinations/braze/ecommerce/tests/index.test.ts:589
- This test name says “minimal fields”, but the expected payload still includes
products,total_value, andmetadata. Consider renaming the test to reflect what it actually validates (e.g., “omits optional cart fields when absent”) or adjust the payload/mapping/expectation so it truly covers the “just cart_id” minimal case described in the PR.
it('should send Cart Updated event with minimal fields correctly', async () => {
const mapping2 = {
...mapping,
name: EVENT_NAMES.CART_UPDATED,
action: undefined,
subtotal_value: undefined,
tax: undefined,
shipping: undefined
}
packages/destination-actions/src/destinations/braze/ecommerce/tests/index.test.ts:1000
- Duplicate assertion; remove one of the
expect(response.length).toBe(1)lines to reduce noise and avoid confusion during future edits.
expect(response.length).toBe(1)
expect(response.length).toBe(1)
packages/destination-actions/src/destinations/braze/ecommerce/generated-types.ts:58
actionis constrained by the UI field choices and runtime casting to'add' | 'remove' | 'replace', but the generated payload type isstring. Tightening this type to a union (and keeping it consistent whereverPayload.actionexists) would prevent invalid values from being passed through and remove the need for repeated casts in the payload builder.
/**
* The cart action that was performed (add, remove, or replace).
*/
action?: string
…ng for testing Syncs the entire braze destination folder from braze-ecommerce-addtocart (PR #3804) onto staging so the ecommerce.cart_updated / add-remove-from-cart work can be tested in the staging environment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.
Suppressed comments (5)
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:1
- PR description states a minimal
cart_updatedtest case with “just cart_id”, buttotal_valueis marked required forCART_UPDATEDhere (and the “minimal” unit test still includestotal_value). Either update the PR description to reflect thattotal_valueis required forcart_updated, or relax this requirement if Braze acceptscart_updatedwithouttotal_value.
import { InputField } from '@segment/actions-core'
packages/destination-actions/src/destinations/braze/index.ts:104
- Now that
ecommerce.cart_updatedis supported (and Product Added/Removed map to it), the generic “Track Calls” preset should also exclude the Segment event name for this flow (typicallyevent != \"Cart Updated\"). OtherwiseCart Updatedtrack calls can still be routed totrackEventinstead of the ecommerce action, causing mis-routing/duplication depending on enabled mappings.
subscribe: 'type = "track" and event != "Order Completed" and event != "Checkout Started" and event != "Order Refunded" and event != "Order Cancelled" and event != "Product Viewed" and event != "Product Added" and event != "Product Removed"',
packages/destination-actions/src/destinations/braze/ecommerce/functions.ts:192
- Casting
actionto'add' | 'remove' | 'replace'bypasses runtime validation and can emit invalid values to Braze if a mapping supplies anything outside the allowed set. Consider validatingactionagainst the allowed literals (e.g., whitelist check) and either omitting the field or throwing a clear error when invalid.
const { cart_id, action, subtotal_value, tax, shipping } = payload as Payload
packages/destination-actions/src/destinations/braze/ecommerce/functions.ts:203
- Casting
actionto'add' | 'remove' | 'replace'bypasses runtime validation and can emit invalid values to Braze if a mapping supplies anything outside the allowed set. Consider validatingactionagainst the allowed literals (e.g., whitelist check) and either omitting the field or throwing a clear error when invalid.
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/tests/index.test.ts:1000
- Duplicate assertion (
expect(response.length).toBe(1)) appears twice in the same test; removing one will reduce noise without changing coverage.
expect(response.length).toBe(1)
expect(response.length).toBe(1)
…updated Regenerates the destination manifest to include the new ecommerce.cart_updated event choice and the action/subtotal_value/tax/shipping fields, keeping the manifest in sync with the field definitions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Brings staging's braze manifest in line with PR #3804 (ecommerce.cart_updated event + action/subtotal_value/tax/shipping fields) for staging testing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.
Suppressed comments (4)
packages/destination-actions/src/destinations/braze/ecommerce/fields.ts:661
- The PR description and test plan refer to sending
catalog_type, but the field defaults to reading$.properties.type(and the tests also setproperties.type). This mismatch will confuse users and likely break expected behavior if they sendproperties.catalog_type; either update the default (and preset mappings/tests) to$.properties.catalog_type, or update the PR description/test plan and field description to explicitly document that the source field isproperties.typeand the destination field becomes Brazeproperties.type.
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' },
packages/destination-actions/src/destinations/braze/ecommerce/functions.ts:205
actionis passed through for any truthy string (theas 'add' | 'remove' | 'replace'cast is compile-time only), so invalid values can still be sent to Braze (e.g., via custom mappings). Consider enforcing the allowed set at runtime (only include when action is exactlyadd|remove|replace, otherwise omit or throw a clear validation error) to prevent sending invalid payloads.
switch(name) {
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/tests/index.test.ts:1000
- This test contains a duplicated assertion (
expect(response.length).toBe(1)) back-to-back. Please remove the duplicate (or replace it with a different assertion that adds coverage) to keep the test intent clear.
expect(response.length).toBe(1)
expect(response.length).toBe(1)
packages/destination-actions/src/destinations/braze/ecommerceSingleProduct/generated-types.ts:50
- This payload type is for
ecommerceSingleProductbut thecart_iddocstring now referencescart_updated, which this action does not emit. Consider adjusting the shared field description to be action-appropriate (or making the description more neutral, e.g., describing cart_id usage without referencing specific events) to reduce confusion for consumers reading the generated types.
* Unique identifier for the cart. Required for cart_updated. For checkout and order events, if no value is passed, Braze will determine a default value for the user cart mapping.
*/
cart_id?: string
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