From b3cce8fa5c938654bd3cbbed7f0038a4832ce48d Mon Sep 17 00:00:00 2001 From: sydneycollins-cio Date: Fri, 7 Aug 2026 08:34:25 -0400 Subject: [PATCH 1/2] fix(customerio): normalize ISO fractional seconds before isIsoDate gate (STRATCONN-4121) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joe Ayoub's review of PR #3923 identified that the fix landed one step too late: convertAttributeTimestamps called isIsoDate(value) on the original string, but in Segment's prod runtime Date.parse returns NaN for >5 fractional digits — so 7-digit timestamps like 2024-08-14T20:36:48.6527521Z never entered the normalization block. Fix: normalize before the gate (normalize -> isIsoDate -> dayjs) so the isIsoDate check sees a valid 3-digit millisecond string regardless of the original precision. Also applies normalizeIsoFractionalSeconds to convertValidTimestamp (same root cause). Tests: adds mocked Date.parse cases that emulate prod runtime behavior, confirming the fix works even when Date.parse rejects long fractions. --- .../customerio/__tests__/utils.test.ts | 47 ++++++++++++++++++- .../src/destinations/customerio/utils.ts | 22 ++++++--- 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts b/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts index 88aab5d388..2446a3ba99 100644 --- a/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts +++ b/packages/destination-actions/src/destinations/customerio/__tests__/utils.test.ts @@ -1,4 +1,4 @@ -import { convertValidTimestamp, resolveIdentifiers, isIsoDate } from '../utils' +import { convertAttributeTimestamps, convertValidTimestamp, resolveIdentifiers, isIsoDate } from '../utils' describe('isIsoDate', () => { it('should return true for valid ISO date with fractional seconds from 1-9 digits', () => { @@ -84,4 +84,49 @@ describe('convertValidTimestamp', () => { it('should leave decimal unix timestamps unchanged', () => { expect(convertValidTimestamp('1712345678.123')).toBe('1712345678.123') }) + + it('should convert a 7-digit fractional second ISO timestamp to unix', () => { + expect(convertValidTimestamp('2024-08-14T20:36:48.6527521Z')).toBe(1723667808) + }) + + it('should convert a 9-digit fractional second ISO timestamp to unix', () => { + expect(convertValidTimestamp('2024-08-14T20:36:48.652752100Z')).toBe(1723667808) + }) + + it('should leave non-date strings unchanged', () => { + expect(convertValidTimestamp('not-a-date')).toBe('not-a-date') + }) +}) + +describe('convertAttributeTimestamps — sub-millisecond fractional seconds (STRATCONN-4121)', () => { + const realParse = Date.parse + afterEach(() => jest.restoreAllMocks()) + + it('converts a 7-digit fractional second timestamp when Date.parse rejects long fractions (prod runtime)', () => { + jest.spyOn(Date, 'parse').mockImplementation((s: string) => { + const m = /\.(\d+)/.exec(s) + return m && m[1].length > 5 ? NaN : realParse(s) + }) + const result = convertAttributeTimestamps({ createdat: '2024-08-14T20:36:48.6527521Z' }) + expect(result.createdat).toBe(1723667808) + }) + + it('converts a 9-digit fractional second timestamp when Date.parse rejects long fractions (prod runtime)', () => { + jest.spyOn(Date, 'parse').mockImplementation((s: string) => { + const m = /\.(\d+)/.exec(s) + return m && m[1].length > 5 ? NaN : realParse(s) + }) + const result = convertAttributeTimestamps({ ts: '2024-08-14T20:36:48.652752100Z' }) + expect(result.ts).toBe(1723667808) + }) + + it('still converts a standard 3-digit millisecond timestamp', () => { + const result = convertAttributeTimestamps({ createdat: '2024-08-14T20:36:48.652Z' }) + expect(result.createdat).toBe(1723667808) + }) + + it('leaves non-date strings unchanged', () => { + const result = convertAttributeTimestamps({ name: 'Acme Corp' }) + expect(result.name).toBe('Acme Corp') + }) }) diff --git a/packages/destination-actions/src/destinations/customerio/utils.ts b/packages/destination-actions/src/destinations/customerio/utils.ts index 94718c56a0..514c439cbc 100644 --- a/packages/destination-actions/src/destinations/customerio/utils.ts +++ b/packages/destination-actions/src/destinations/customerio/utils.ts @@ -37,6 +37,12 @@ export enum AccountRegion { EU = 'EU 🇪🇺' } +// Normalize ISO timestamp fractional seconds to 3 digits so dayjs parses reliably. +// dayjs only handles millisecond precision; sub-millisecond digits cause silent +// misparse. Unix timestamps are second-level anyway so no precision is lost. +const normalizeIsoFractionalSeconds = (value: string): string => + value.replace(/(\.\d{3})\d+(Z|[+-]\d{2}:?\d{2}|$)/, '$1$2') + export const convertValidTimestamp = (value: Value): Value | number => { // Timestamps may be on a `string` field, so check if the string is only // digits (optionally with a fractional part). If it is, ignore it since @@ -47,7 +53,7 @@ export const convertValidTimestamp = (value: Value): Value | nu return value } - const maybeDate = dayjs.utc(value) + const maybeDate = dayjs.utc(normalizeIsoFractionalSeconds(value)) if (maybeDate.isValid()) { return maybeDate.unix() @@ -66,11 +72,15 @@ export const convertAttributeTimestamps = (payload: Payload) const value = payload[key] if (typeof value === 'string') { - // Parse only ISO 8601 date formats in strict mode - const maybeDate = dayjs(value) - - if (isIsoDate(value)) { - ;(clone[key] as unknown) = maybeDate.unix() + // Normalize sub-millisecond fractional seconds BEFORE the isIsoDate gate. + // In Segment's prod runtime, Date.parse returns NaN for >5 fractional digits, + // so isIsoDate(value) would return false and normalization would never run. + // Normalizing first ensures the gate sees a valid parseable string. + // See: STRATCONN-4121, PR #3923 review feedback from joe-ayoub-segment. + const normalized = normalizeIsoFractionalSeconds(value) + + if (isIsoDate(normalized)) { + ;(clone[key] as unknown) = dayjs(normalized).unix() return } } From ace2af07e736a274d7c20613b6a7e54ce41c46dc Mon Sep 17 00:00:00 2001 From: sydneycollins-cio Date: Fri, 7 Aug 2026 11:15:03 -0400 Subject: [PATCH 2/2] ci: trigger fresh check run