From 3e2981109abd24927149a86b89b2b7ea93317adc Mon Sep 17 00:00:00 2001 From: Scott Lepich Date: Mon, 6 Jul 2026 16:24:18 -0700 Subject: [PATCH 1/3] fix(ms-bing-capi): item price should be number not integer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `items` field defined per-item `price` with `type: 'integer'`, which is too strict. Microsoft's UET Conversions API accepts DECIMAL item prices in whole currency units — their official docs show the `price` parameter example as 25.1 and every JSON sample uses decimals such as "price": 25.1 and "price": 27.3 (https://learn.microsoft.com/en-us/advertising/guides/uet-conversion-api-integration). With `type: 'integer'`, Segment's mapping-kit rejected valid decimal prices (e.g. 9.99) with "400: Item Price must be an integer but it was a number", dropping the entire event. Changing the type to `number` allows decimal prices through. `quantity` remains an integer. generated-types.ts is unchanged since both integer and number map to the TypeScript type `number`. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sendEvent/__tests__/sendEvent.test.ts | 28 +++++++++++++++++++ .../ms-bing-capi/sendEvent/fields.ts | 2 +- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts index cf201f941b3..327a10c51d5 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts @@ -210,6 +210,34 @@ describe('Microsoft Bing CAPI (Actions) - sendEvent (updated)', () => { expect(scope.isDone()).toBe(true) }) + test('decimal item price is accepted and forwarded (not rejected as non-integer)', async () => { + const event = buildTrackEvent() + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`, (body: any) => { + const items = body.data[0].customData.items + expect(items).toHaveLength(1) + expect(items[0].price).toBe(9.99) + expect(items[0].quantity).toBe(2) + expect(items[0].id).toBe('sku-1') + return true + }) + .reply(200, {}) + const responses: any = await testDestination.testAction('sendEvent', { + event, + settings, + mapping: { + data: { eventType: 'custom', eventTime: new Date('2024-01-01T00:00:00.000Z').toISOString() }, + customData: { value: 9.99 }, + items: [{ id: 'sku-1', name: 'Widget', price: 9.99, quantity: 2 }], + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + // testAction resolves (no mapping-kit validation error thrown) for a decimal price + expect(responses[0].status).toBe(200) + expect(scope.isDone()).toBe(true) + }) + test('pageLoad event requires page context mapping and is sent correctly', async () => { const iso = '2024-06-01T12:00:00.000Z' const event = buildTrackEvent({ type: 'page', event: undefined }) diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts index 2b1309a1072..c55b52a7cd3 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/fields.ts @@ -196,7 +196,7 @@ export const items: InputField = { price: { label: 'Item Price', description: 'The price of the item, after discounts.', - type: 'integer' + type: 'number' }, quantity: { label: 'Item Quantity', From 876f0c6f08ab5e3d58bf5847f1b01df3f216186b Mon Sep 17 00:00:00 2001 From: Scott Lepich Date: Tue, 7 Jul 2026 12:18:41 -0700 Subject: [PATCH 2/3] fix(ms-bing-capi): do not fail batched events on validation warnings The sendEvent action sends events with `continueOnValidationError: true`, so Microsoft's CAPI accepts an event (HTTP 200, `eventsReceived: 1`) even when it has non-fatal issues, and reports those issues as entries in `error.details[]` flagged with `"isWarning": true`. The batch response handler (`performBatch`) matched `error.details[]` by `index` only and treated ANY matching detail as a hard failure. As a result, a batched event whose only detail was a warning was marked `status: 400` and reported as a failed delivery, even though Microsoft had accepted it. This caused real, silent delivery failures for any batched event that triggered a Microsoft warning. (Single `perform` returns the raw 200, so the bug only manifested in batching.) Fix: only treat a detail as a failure when it is NOT a warning (`detail.index === index && !detail.isWarning`). A warning-only event is now marked SUCCESS (200); an event with a real (non-warning) error at its index is still marked 400. `types.ts` is extended to model the `isWarning` and `errorCode` fields the API actually returns. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../sendEvent/__tests__/sendEvent.test.ts | 102 ++++++++++++++++++ .../ms-bing-capi/sendEvent/index.ts | 2 +- .../ms-bing-capi/sendEvent/types.ts | 2 + 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts index 327a10c51d5..a66b4da0aa5 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/__tests__/sendEvent.test.ts @@ -420,6 +420,108 @@ describe('Microsoft Bing CAPI (Actions) - sendEvent (updated)', () => { expect(scope.isDone()).toBe(true) }) + test('batch: warning-only detail (isWarning:true) at an index is treated as SUCCESS, not failure', async () => { + const events = [buildTrackEvent({ messageId: 'm1' }), buildTrackEvent({ messageId: 'm2' })] + // Microsoft returns HTTP 200 and, because continueOnValidationError:true, reports + // non-fatal issues as warnings while still accepting the event. + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`) + .reply(200, { + eventsReceived: 2, + error: { + details: [ + { + errorCode: 'Empty', + errorMessage: "'price' must not be empty.", + index: 0, + isWarning: true, + propertyName: 'data[0].customData.items[0].price' + } + ] + } + }) + const responses: any = await testDestination.executeBatch('sendEvent', { + events, + settings, + mapping: { + enable_batching: true, + data: { eventType: 'custom' }, + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + expect(responses.length).toBe(2) + // Warning-only event was accepted by Microsoft -> must be a success, not a 400. + expect(responses[0].status).toBe(200) + expect(responses[1].status).toBe(200) + expect(scope.isDone()).toBe(true) + }) + + test('batch: real error (isWarning:false) is still marked 400 while warning at another index stays 200', async () => { + const events = [buildTrackEvent({ messageId: 'm1' }), buildTrackEvent({ messageId: 'm2' })] + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`) + .reply(200, { + eventsReceived: 1, + error: { + details: [ + { + errorCode: 'Empty', + errorMessage: "'price' must not be empty.", + index: 0, + isWarning: true, + propertyName: 'data[0].customData.items[0].price' + }, + { + errorCode: 'Invalid', + errorMessage: 'Second failed', + index: 1, + isWarning: false, + propertyName: 'data[1].eventName' + } + ] + } + }) + const responses: any = await testDestination.executeBatch('sendEvent', { + events, + settings, + mapping: { + enable_batching: true, + data: { eventType: 'custom' }, + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + expect(responses.length).toBe(2) + // index 0 had only a warning -> success; index 1 had a real error -> failure. + expect(responses[0].status).toBe(200) + expect(responses[1].status).toBe(400) + expect(responses[1].errormessage).toContain('Second failed') + expect(scope.isDone()).toBe(true) + }) + + test('batch: error detail without isWarning field defaults to a real failure (400)', async () => { + const events = [buildTrackEvent({ messageId: 'm1' }), buildTrackEvent({ messageId: 'm2' })] + const scope = nock('https://capi.uet.microsoft.com') + .post(`/v1/${settings.UetTag}/events`) + .reply(200, { error: { details: [{ index: 1, errorMessage: 'Second failed' }] } }) + const responses: any = await testDestination.executeBatch('sendEvent', { + events, + settings, + mapping: { + enable_batching: true, + data: { eventType: 'custom' }, + userData: { anonymousId: 'anon-1' }, + timestamp: { '@path': '$.timestamp' } + } + }) + expect(responses.length).toBe(2) + expect(responses[0].status).toBe(200) + expect(responses[1].status).toBe(400) + expect(responses[1].errormessage).toContain('Second failed') + expect(scope.isDone()).toBe(true) + }) + test('phone digits normalized before hashing', async () => { const rawPhone = '+1 (555) 123-4567 ext.89' const event = buildTrackEvent({ context: { traits: { phone: rawPhone, email: 'norm@example.com' } } }) diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts index 6c1bd66b51a..48279b305b4 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/index.ts @@ -72,7 +72,7 @@ async function send(request: RequestClient, payloads: Payload[], settings: Setti const details = response.data?.error?.details ?? [] payloads.forEach((payload, index) => { - const error = details.find((detail) => detail.index === index) + const error = details.find((detail) => detail.index === index && !detail.isWarning) if (error) { multiStatusResponse.setErrorResponseAtIndex(index, { status: 400, diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts index dbd69b456ab..59690b29ef3 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts +++ b/packages/destination-actions/src/destinations/ms-bing-capi/sendEvent/types.ts @@ -61,6 +61,8 @@ export interface MSMultiStatusResponse { propertyName: string attemptedValue: unknown errorMessage: string + errorCode?: string + isWarning?: boolean }> } traceId: string From 0a93f1074285d27878e1423282e3d12bdff19f0e Mon Sep 17 00:00:00 2001 From: Scott Lepich Date: Wed, 29 Jul 2026 12:41:27 -0700 Subject: [PATCH 3/3] chore(ms-bing-capi): regenerate metadata.json for Item Price type change The sendEvent `items.price` field type was changed from `integer` to `number` in fields.ts, but metadata.json was not regenerated. Running `yarn generate:metadata-payload` updates the Item Price field type to `number`, which fixes the failing "Assert metadata payloads are up-to-date" CI check. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/destinations/ms-bing-capi/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json b/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json index 80683917d28..3bd81d893db 100644 --- a/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json +++ b/packages/destination-actions/src/destinations/ms-bing-capi/metadata.json @@ -1843,7 +1843,7 @@ "price": { "label": "Item Price", "description": "The price of the item, after discounts.", - "type": "integer", + "type": "number", "required": false, "multiple": false, "allowNull": false,