From 509097d84c43652dd3b6edfef9763c8931730e59 Mon Sep 17 00:00:00 2001 From: John Greene Date: Fri, 2 Jan 2026 08:50:59 -0800 Subject: [PATCH] correctly cast non-date items --- .../airship/__tests__/utilities.test.ts | 63 +++++++++++++++++++ .../src/destinations/airship/utilities.ts | 25 +++++++- 2 files changed, 85 insertions(+), 3 deletions(-) diff --git a/packages/destination-actions/src/destinations/airship/__tests__/utilities.test.ts b/packages/destination-actions/src/destinations/airship/__tests__/utilities.test.ts index d94ddf4c6d7..6f06f46f273 100644 --- a/packages/destination-actions/src/destinations/airship/__tests__/utilities.test.ts +++ b/packages/destination-actions/src/destinations/airship/__tests__/utilities.test.ts @@ -99,6 +99,55 @@ describe('Testing _build_attribute_object', () => { it('should correctly format an attribute', () => { expect(_private._build_attributes_object(valid_attributes_payload)).toEqual(airship_attributes_payload) }) + + it('should NOT parse non-date string attributes as dates', () => { + const payload: AttributesPayload = { + named_user_id: 'test-user', + occurred: occurred.toISOString(), + attributes: { + shop_last_store_name: 'SOUMAGNE 2', + shop_visited_store_names: 'BARCHON | HOGNOUL | JEMEPPE | SOUMAGNE 2', + shop_last_store_id: '30290' + } + } + const result = _private._build_attributes_object(payload) + + // These should remain as strings, not be converted to dates + const shopNameAttr = result.find((attr: any) => attr.key === 'shop_last_store_name') + expect(shopNameAttr?.value).toBe('SOUMAGNE 2') + + const shopNamesAttr = result.find((attr: any) => attr.key === 'shop_visited_store_names') + expect(shopNamesAttr?.value).toBe('BARCHON | HOGNOUL | JEMEPPE | SOUMAGNE 2') + + const shopIdAttr = result.find((attr: any) => attr.key === 'shop_last_store_id') + expect(shopIdAttr?.value).toBe('30290') + }) + + it('should still parse date-like strings as dates', () => { + const payload: AttributesPayload = { + named_user_id: 'test-user', + occurred: occurred.toISOString(), + attributes: { + birthdate: '1965-01-25T00:47:43.378Z', + account_creation: '2023-05-09T00:47:43.378Z', + custom_date_field: '2025-06-11', + another_date: '01/25/1965' + } + } + const result = _private._build_attributes_object(payload) + + const birthdateAttr = result.find((attr: any) => attr.key === 'birthdate') + expect(birthdateAttr?.value).toBe('1965-01-25T00:47:43') + + const accountCreationAttr = result.find((attr: any) => attr.key === 'account_creation') + expect(accountCreationAttr?.value).toBe('2023-05-09T00:47:43') + + const customDateAttr = result.find((attr: any) => attr.key === 'custom_date_field') + expect(customDateAttr?.value).toMatch(/2025-06-11/) + + const anotherDateAttr = result.find((attr: any) => attr.key === 'another_date') + expect(anotherDateAttr?.value).toMatch(/1965-01-25/) + }) }) describe('Testing _build_tags_object', () => { @@ -131,6 +180,20 @@ describe('Testing _parse_date', () => { it('should parse a date-looking string into a date object', () => { expect(_private._parse_date('2025-06-11')).toBeInstanceOf(Date) }) + + it('should NOT parse strings without date-like patterns', () => { + expect(_private._parse_date('SOUMAGNE 2')).toBeNull() + expect(_private._parse_date('BARCHON | HOGNOUL | JEMEPPE | SOUMAGNE 2')).toBeNull() + expect(_private._parse_date('30290')).toBeNull() + expect(_private._parse_date('SOUMAGNE')).toBeNull() + }) + + it('should parse valid date formats', () => { + expect(_private._parse_date('2023-05-09T00:47:43.378Z')).toBeInstanceOf(Date) + expect(_private._parse_date('2025-06-11')).toBeInstanceOf(Date) + expect(_private._parse_date('01/25/1965')).toBeInstanceOf(Date) + expect(_private._parse_date('1965-01-25')).toBeInstanceOf(Date) + }) }) describe('Testing _parse_and_format_date', () => { diff --git a/packages/destination-actions/src/destinations/airship/utilities.ts b/packages/destination-actions/src/destinations/airship/utilities.ts index e7e355fbf8a..5afb13240f2 100644 --- a/packages/destination-actions/src/destinations/airship/utilities.ts +++ b/packages/destination-actions/src/destinations/airship/utilities.ts @@ -377,20 +377,39 @@ function _parse_and_format_date(date_string: string) { function _parse_date(attribute_value: any): Date | null { /* This function is for converting dates or returning null if they're not valid. + It requires the string to contain date-like patterns to avoid false positives. */ if (attribute_value.length < 8) { return null // Reduce false positive dates } + // Require the string to contain date-like patterns to avoid false positives + // Common date separators: dashes, slashes, colons, spaces with numbers + // ISO format patterns: YYYY-MM-DD, YYYY-MM-DDTHH:mm:ss, etc. + const dateLikePattern = /(\d{4}[-\/]\d{1,2}[-\/]\d{1,2})|(\d{1,2}[-\/]\d{1,2}[-\/]\d{4})|(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})|(\d{1,2}\/\d{1,2}\/\d{4})/ + + // If the string doesn't contain date-like patterns, don't parse it as a date + if (!dateLikePattern.test(attribute_value)) { + return null + } + // Attempt to parse the attribute_value as a Date const date = new Date(attribute_value) // Check if the parsing was successful and the result is a valid date - if (!isNaN(date.getTime())) { - return date // Return the parsed Date + if (isNaN(date.getTime())) { + return null + } + + // Additional validation: check if the parsed date is reasonable + // Reject dates that are too far in the past (before 1900) or future (after 2100) + // This helps catch cases where JavaScript's Date constructor makes unexpected interpretations + const year = date.getFullYear() + if (year < 1900 || year > 2100) { + return null } - return null // Return null for invalid dates + return date // Return the parsed Date } function _extract_country_language(locale: string): string[] {