diff --git a/packages/browser-destinations/destinations/cj/package.json b/packages/browser-destinations/destinations/cj/package.json new file mode 100644 index 00000000000..e72c8d9866d --- /dev/null +++ b/packages/browser-destinations/destinations/cj/package.json @@ -0,0 +1,24 @@ +{ + "name": "@segment/analytics-browser-actions-cj", + "version": "1.0.0", + "license": "MIT", + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org" + }, + "main": "./dist/cjs", + "module": "./dist/esm", + "scripts": { + "build": "yarn build:esm && yarn build:cjs", + "build:cjs": "tsc --module commonjs --outDir ./dist/cjs", + "build:esm": "tsc --outDir ./dist/esm" + }, + "typings": "./dist/esm", + "dependencies": { + "@segment/actions-core": "^3.158.0", + "@segment/browser-destination-runtime": "^1.87.0" + }, + "peerDependencies": { + "@segment/analytics-next": ">=1.55.0" + } +} diff --git a/packages/browser-destinations/destinations/cj/src/generated-types.ts b/packages/browser-destinations/destinations/cj/src/generated-types.ts new file mode 100644 index 00000000000..5f4c7605be3 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/generated-types.ts @@ -0,0 +1,12 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Settings { + /** + * Your Commission Junction Tag ID. + */ + tagId: string + /** + * Used with the "Order" Action only. Can be overridden at the Action level. This is a static value provided by CJ. Each account may have multiple actions and each will be referenced by a different actionTrackerId value. + */ + actionTrackerId?: string +} diff --git a/packages/browser-destinations/destinations/cj/src/index.ts b/packages/browser-destinations/destinations/cj/src/index.ts new file mode 100644 index 00000000000..eae1a18f795 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/index.ts @@ -0,0 +1,46 @@ +import type { BrowserDestinationDefinition } from '@segment/browser-destination-runtime/types' +import { browserDestination } from '@segment/browser-destination-runtime/shim' +import type { Settings } from './generated-types' +import { CJ } from './types' +import sitePage from './sitePage' +import order from './order' + +declare global { + interface Window { + cj: CJ + } +} + +export const destination: BrowserDestinationDefinition = { + name: 'Commission Junction (Actions)', + slug: 'actions-cj', + mode: 'device', + description: + 'The Commission Junction Browser Destination allows you to install the CJ Javascript pixel onto your site and pass mapped Segment events and metadata to CJ.', + settings: { + tagId: { + label: 'Tag ID', + description: 'Your Commission Junction Tag ID.', + type: 'string', + required: true + }, + actionTrackerId: { + label: 'Action Tracker ID', + description: + 'Used with the "Order" Action only. Can be overridden at the Action level. This is a static value provided by CJ. Each account may have multiple actions and each will be referenced by a different actionTrackerId value.', + type: 'string' + } + }, + initialize: async () => { + if (!window.cj) { + window.cj = {} as CJ + } + return window.cj + }, + actions: { + sitePage, + order + } +} + +export default browserDestination(destination) diff --git a/packages/browser-destinations/destinations/cj/src/order/__tests__/index.test.ts b/packages/browser-destinations/destinations/cj/src/order/__tests__/index.test.ts new file mode 100644 index 00000000000..4a2b5bbbe50 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/order/__tests__/index.test.ts @@ -0,0 +1,985 @@ +import { Analytics, Context } from '@segment/analytics-next' +import { Subscription } from '@segment/browser-destination-runtime' +import CJDestination, { destination } from '../../index' +import { CJ } from '../../types' +import * as sendModule from '../../utils' +import * as orderModule from '../utils' +import { allVerticals, travelVerticals, financeVerticals, networkServicesVerticals } from '../order-fields' + +describe('CJ init', () => { + const settings = { + tagId: '123456789', + actionTrackerId: '987654321' + } + + const testCookieName = 'cjeventOrder' + let mockCJ: CJ + let orderEvent: any + beforeEach(async () => { + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockCJ = {} as CJ + return Promise.resolve(mockCJ) + }) + + document.cookie = `${testCookieName}=testcCookieValue` + + jest.spyOn(sendModule, 'send').mockImplementation(() => { + return Promise.resolve() + }) + + jest.spyOn(orderModule, 'setOrderJSON') + + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + test('CJ pixel order event', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'order', + name: 'order', + enabled: true, + subscribe: 'type = "track" and event = "Order Completed"', + mapping: { + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + orderId: { '@path': '$.properties.order_id' }, + currency: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.total' }, + discount: { '@path': '$.properties.discount' }, + coupon: { '@path': '$.properties.coupon' }, + cjeventOrderCookieName: testCookieName, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + } + } + } + ] + const context = new Context({ + type: 'track', + event: 'Order Completed', + userId: 'userId-abc123', + context: { + traits: { + email: 'test@test.com' + } + }, + properties: { + order_id: 'abc12345', + currency: 'USD', + coupon: 'COUPON1', + quantity: 5, + total: 10.99, + discount: 1, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ] + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const orderJSON = { + trackingSource: 'Segment', + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a', + orderId: 'abc12345', + actionTrackerId: "987654321", + currency: 'USD', + amount: 10.99, + discount: 1, + coupon: 'COUPON1', + cjeventOrder: 'testcCookieValue', + items:[ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ] + } + + orderEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await orderEvent.load(Context.system(), {} as Analytics) + await orderEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalledWith( + {}, + orderJSON + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) + + test('CJ pixel order event with pre-hashed email', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'order', + name: 'order', + enabled: true, + subscribe: 'type = "track" and event = "Order Completed"', + mapping: { + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + orderId: { '@path': '$.properties.order_id' }, + currency: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.total' }, + discount: { '@path': '$.properties.discount' }, + coupon: { '@path': '$.properties.coupon' }, + cjeventOrderCookieName: testCookieName, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + } + } + } + ] + const context = new Context({ + type: 'track', + event: 'Order Completed', + userId: 'userId-abc123', + context: { + traits: { + email: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a' + } + }, + properties: { + order_id: 'abc12345', + currency: 'USD', + coupon: 'COUPON1', + quantity: 5, + total: 10.99, + discount: 1, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ] + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const orderJSON = { + trackingSource: 'Segment', + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a', + orderId: 'abc12345', + actionTrackerId: "987654321", + currency: 'USD', + amount: 10.99, + discount: 1, + coupon: 'COUPON1', + cjeventOrder: 'testcCookieValue', + items:[ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ] + } + + orderEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await orderEvent.load(Context.system(), {} as Analytics) + await orderEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalledWith( + {}, + orderJSON + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) + + test('CJ pixel order event with All Verticals field data', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'order', + name: 'order', + enabled: true, + subscribe: 'type = "track" and event = "Order Completed"', + mapping: { + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + orderId: { '@path': '$.properties.order_id' }, + currency: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.total' }, + discount: { '@path': '$.properties.discount' }, + coupon: { '@path': '$.properties.coupon' }, + cjeventOrderCookieName: testCookieName, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + }, + allVerticals: allVerticals.default + } + } + ] + const context = new Context({ + type: 'track', + event: 'Order Completed', + userId: 'userId-abc123', + context: { + traits: { + email: 'test@test.com' + }, + locale: 'en-US' + }, + properties: { + order_id: 'abc12345', + currency: 'USD', + coupon: 'COUPON1', + quantity: 5, + total: 10.99, + discount: 1, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ], + // All Verticals fields + brand: 'Nike', + brand_id: 'BR123', + business_unit: 'Online', + campaign_id: 'CAMP456', + campaign_name: 'BackToSchool', + category: 'Footwear', + class: 'Premium', + confirmation_number: 999888777, + coupon_discount: 15, + coupon_type: 'percent', + customer_country: 'en-US', + customer_segment: 'Loyal', + customer_status: 'Return', + customer_type: 'GroupBuyer', + delivery: 'STANDARD', + description: 'Running shoes', + duration: 7, + end_date_time: '2025-08-06T18:30:00Z', + genre: 'Sports', + item_id: 'ITEM999', + item_name: 'Air Max', + item_type: 'Sneakers', + lifestage: 'Adult', + location: 'NY', + loyalty_earned: 200, + loyalty_first_time_signup: 'Yes', + loyalty_level: 'Gold', + loyalty_redeemed: 50, + loyalty_status: 'Yes', + margin: '15%', + marketing_channel: 'affiliate', + no_cancellation: 'No', + order_subtotal: 120, + payment_method: 'credit_debit_card', + payment_model: 'OneTime', + platform_id: 'ios', + point_of_sale: 'INTERNET', + preorder: 'No', + prepaid: 'Yes', + promotion: 'SUMMER20', + promotion_amount: 20, + promotion_condition_threshold: 100, + promotion_condition_type: 'LOYALTY_REQUIRED', + promotion_ends: '2025-08-10T00:00:00Z', + promotion_starts: '2025-08-01T00:00:00Z', + promotion_type: 'PERCENT_OFF', + rating: '4.5', + service_type: 'wireless', + start_date_time: '2025-08-05T10:00:00Z', + subscription_fee: 9.99, + subscription_length: '12 months', + tax_amount: 10, + tax_type: 'STATE', + upsell: 'Yes' + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const orderJSON = { + trackingSource: 'Segment', + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a', + orderId: 'abc12345', + actionTrackerId: '987654321', + currency: 'USD', + amount: 10.99, + discount: 1, + coupon: 'COUPON1', + cjeventOrder: 'testcCookieValue', + items: [ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ], + // All Verticals fields + brand: 'Nike', + brandId: 'BR123', + businessUnit: 'Online', + campaignId: 'CAMP456', + campaignName: 'BackToSchool', + category: 'Footwear', + class: 'Premium', + confirmationNumber: 999888777, + couponDiscount: 15, + couponType: 'percent', + customerCountry: 'US', + customerSegment: 'Loyal', + customerStatus: 'Return', + customerType: 'GroupBuyer', + delivery: 'STANDARD', + description: 'Running shoes', + duration: 7, + endDateTime: '2025-08-06T18:30:00Z', + genre: 'Sports', + itemId: 'ITEM999', + itemName: 'Air Max', + itemType: 'Sneakers', + lifestage: 'Adult', + location: 'NY', + loyaltyEarned: 200, + loyaltyFirstTimeSignup: 'Yes', + loyaltyLevel: 'Gold', + loyaltyRedeemed: 50, + loyaltyStatus: 'Yes', + margin: '15%', + marketingChannel: 'affiliate', + noCancellation: 'No', + orderSubtotal: 120, + paymentMethod: 'credit_debit_card', + paymentModel: 'OneTime', + platformId: 'ios', + pointOfSale: 'INTERNET', + preorder: 'No', + prepaid: 'Yes', + promotion: 'SUMMER20', + promotionAmount: 20, + promotionConditionThreshold: 100, + promotionConditionType: 'LOYALTY_REQUIRED', + promotionEnds: '2025-08-10T00:00:00Z', + promotionStarts: '2025-08-01T00:00:00Z', + promotionType: 'PERCENT_OFF', + quantity: 5, + rating: '4.5', + serviceType: 'wireless', + startDateTime: '2025-08-05T10:00:00Z', + subscriptionFee: 9.99, + subscriptionLength: '12 months', + taxAmount: 10, + taxType: 'STATE', + upsell: 'Yes' + } + + orderEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await orderEvent.load(Context.system(), {} as Analytics) + await orderEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalledWith( + {}, + orderJSON + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) + + test('CJ pixel order event with Travel Vertical data', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'order', + name: 'order', + enabled: true, + subscribe: 'type = "track" and event = "Order Completed"', + mapping: { + verticalType: 'travel', + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + orderId: { '@path': '$.properties.order_id' }, + currency: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.total' }, + discount: { '@path': '$.properties.discount' }, + coupon: { '@path': '$.properties.coupon' }, + cjeventOrderCookieName: testCookieName, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + }, + travelVerticals: travelVerticals.default + } + } + ] + const context = new Context({ + type: 'track', + event: 'Order Completed', + userId: 'userId-abc123', + context: { + traits: { + email: 'test@test.com' + } + }, + properties: { + order_id: 'abc12345', + currency: 'USD', + coupon: 'COUPON1', + quantity: 5, + total: 10.99, + discount: 1, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ], + booking_date: '2025-08-06T18:30:00Z', + booking_status: 'Booking Status Test Value', + booking_value_post_tax: 100, + booking_value_pre_tax: 900, + car_options: 'insurance', + class: 'first', + cruise_type: 'Alaskan', + destination_city: 'London', + destination_country: 'UK', + destination_state: 'France', + domestic: 'NO', + dropoff_iata: 'DUB', + dropoff_id: 'CDG', + flight_fare_type: 'gotta get away', + flight_options: 'wifi', + flight_type: 'ROUND_TRIP', + flyer_miles: 8000, + guests: 2, + iata: 'CDG,LHR,DUB', + itinerary_id: 'itinerary_id_1', + minimum_stay_duration: 9, + origin_city: 'DUB', + origin_country: 'IE', + origin_state: 'US-AK', + paid_at_booking_post_tax: 100, + paid_at_booking_pre_tax: 90, + pickup_iata: 'LRH', + pickup_id: 'test_pickup+id', + port: 'SYD', + room_type: 'Double room', + rooms: 3, + ship_name: 'Titanic', + travel_type: 'CRUISE' + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const orderJSON = { + trackingSource: 'Segment', + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a', + orderId: 'abc12345', + actionTrackerId: '987654321', + currency: 'USD', + amount: 10.99, + discount: 1, + coupon: 'COUPON1', + cjeventOrder: 'testcCookieValue', + items: [ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ], + bookingDate: '2025-08-06T18:30:00Z', + bookingStatus: 'Booking Status Test Value', + bookingValuePostTax: 100, + bookingValuePreTax: 900, + carOptions: 'insurance', + class: 'first', + cruiseType: 'Alaskan', + destinationCity: 'London', + destinationCountry: 'UK', + destinationState: 'France', + domestic: 'NO', + dropoffIata: 'DUB', + dropoffId: 'CDG', + flightFareType: 'gotta get away', + flightOptions: 'wifi', + flightType: 'ROUND_TRIP', + flyerMiles: 8000, + guests: 2, + iata: 'CDG,LHR,DUB', + itineraryId: 'itinerary_id_1', + minimumStayDuration: 9, + originCity: 'DUB', + originCountry: 'IE', + originState: 'US-AK', + paidAtBookingPostTax: 100, + paidAtBookingPreTax: 90, + pickupIata: 'LRH', + pickupId: 'test_pickup+id', + port: 'SYD', + roomType: 'Double room', + rooms: 3, + shipName: 'Titanic', + travelType: 'CRUISE' + } + + orderEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await orderEvent.load(Context.system(), {} as Analytics) + await orderEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalledWith( + {}, + orderJSON + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) + + test('CJ pixel order event with Finance Vertical data', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'order', + name: 'order', + enabled: true, + subscribe: 'type = "track" and event = "Order Completed"', + mapping: { + verticalType: 'finance', + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + orderId: { '@path': '$.properties.order_id' }, + currency: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.total' }, + discount: { '@path': '$.properties.discount' }, + coupon: { '@path': '$.properties.coupon' }, + cjeventOrderCookieName: testCookieName, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + }, + financeVerticals: financeVerticals.default + } + } + ] + const context = new Context({ + type: 'track', + event: 'Order Completed', + userId: 'userId-abc123', + context: { + traits: { + email: 'test@test.com' + } + }, + properties: { + order_id: 'abc12345', + currency: 'USD', + coupon: 'COUPON1', + quantity: 5, + total: 10.99, + discount: 1, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ], + annual_fee: 100, + application_status: 'instant_approved', + apr: 0.4, + apr_transfer: 'test_aprTransfer', + apr_transfer_time: 5, + card_category: 'BALANCE_TRANSFER_CARDS', + cash_advance_fee: 5, + contract_length: 7, + contract_type: 'test contractType', + credit_report: 'purchase', + credit_line: 4, + credit_quality: 'Very Poor', + funded_amount: 200, + funded_currency: 'USD', + introductory_apr: 5, + introductory_apr_time: 2, + minimum_balance: 500, + minimum_deposit: { '@path': '$.properties.minimum_deposit' }, + prequalify: 'YES', + transfer_fee: 80 + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const orderJSON = { + trackingSource: 'Segment', + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a', + orderId: 'abc12345', + actionTrackerId: '987654321', + currency: 'USD', + amount: 10.99, + discount: 1, + coupon: 'COUPON1', + cjeventOrder: 'testcCookieValue', + items: [ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ], + annualFee: 100, + applicationStatus: 'instant_approved', + apr: 0.4, + aprTransfer: 'test_aprTransfer', + aprTransferTime: 5, + cardCategory: 'BALANCE_TRANSFER_CARDS', + cashAdvanceFee: 5, + contractLength: 7, + contractType: 'test contractType', + creditReport: 'purchase', + creditLine: 4, + creditQuality: 'Very Poor', + fundedAmount: 200, + fundedCurrency: 'USD', + introductoryApr: 5, + introductoryAprTime: 2, + minimumBalance: 500, + minimumDeposit: { '@path': '$.properties.minimum_deposit' }, + prequalify: 'YES', + transferFee: 80 + } + + orderEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await orderEvent.load(Context.system(), {} as Analytics) + await orderEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalledWith( + {}, + orderJSON + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) + + test('CJ pixel order event with Network Service Vertical data', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'order', + name: 'order', + enabled: true, + subscribe: 'type = "track" and event = "Order Completed"', + mapping: { + verticalType: 'network', + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + }, + orderId: { '@path': '$.properties.order_id' }, + currency: { '@path': '$.properties.currency' }, + amount: { '@path': '$.properties.total' }, + discount: { '@path': '$.properties.discount' }, + coupon: { '@path': '$.properties.coupon' }, + cjeventOrderCookieName: testCookieName, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + }, + networkServicesVerticals: networkServicesVerticals.default + } + } + ] + const context = new Context({ + type: 'track', + event: 'Order Completed', + userId: 'userId-abc123', + context: { + traits: { + email: 'test@test.com' + } + }, + properties: { + order_id: 'abc12345', + currency: 'USD', + coupon: 'COUPON1', + quantity: 5, + total: 10.99, + discount: 1, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ], + annual_fee: 10, + application_status: 'instant_approved', + contract_length: 2, + contract_type: 'test contract type' + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const orderJSON = { + trackingSource: 'Segment', + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'conversionConfirmation', + emailHash: 'f660ab912ec121d1b1e928a0bb4bc61b15f5ad44d5efdc4e1c92a25e99b8e44a', + orderId: 'abc12345', + actionTrackerId: '987654321', + currency: 'USD', + amount: 10.99, + discount: 1, + coupon: 'COUPON1', + cjeventOrder: 'testcCookieValue', + items: [ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ], + annualFee: 10, + applicationStatus: 'instant_approved', + contractLength: 2, + contractType: 'test contract type' + } + + orderEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await orderEvent.load(Context.system(), {} as Analytics) + await orderEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalled() + expect(orderModule.setOrderJSON).toHaveBeenCalledWith( + {}, + orderJSON + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) + +}) diff --git a/packages/browser-destinations/destinations/cj/src/order/generated-types.ts b/packages/browser-destinations/destinations/cj/src/order/generated-types.ts new file mode 100644 index 00000000000..bf19bc5b1b9 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/order/generated-types.ts @@ -0,0 +1,545 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * Send additional data relating to travel, finance network services + */ + verticalType?: string + /** + * A unique ID assigned by you to the user. + */ + userId?: string + /** + * Your CJ Enterprise ID. + */ + enterpriseId: number + /** + * Page type to be sent to CJ. + */ + pageType: string + /** + * Segment will ensure the email address is hashed before sending to CJ. + */ + emailHash?: string + /** + * The orderId is a unique identifier, such as an order identifier or invoice number, which must be populated for each order. + */ + orderId: string + /** + * Required if not specified in Settings. This is a static value provided by CJ. Each account may have multiple actions and each will be referenced by a different actionTrackerId value. + */ + actionTrackerId?: string + /** + * The currency of the order, e.g. USD, EUR. + */ + currency: string + /** + * The total amount of the order. This should exclude shipping or tax. + */ + amount: number + /** + * The total discount applied to the order. + */ + discount?: number + /** + * The coupon code applied to the order. + */ + coupon?: string + /** + * The name of the cookie that stores the CJ Event ID. This is required whenever the advertiser uses their own cookie to store the Event ID. + */ + cjeventOrderCookieName: string + /** + * The items to be sent to CJ. + */ + items?: { + /** + * the price of the item before tax and discount. + */ + unitPrice: number + /** + * The item sku. + */ + itemId: string + /** + * The quantity of the item. + */ + quantity: number + /** + * The discount applied to the item. + */ + discount?: number + }[] + /** + * This field is used to pass additional parameters specific to the vertical. All vertical parameters listed below can be utilized regardless of the account's vertical. + */ + allVerticals?: { + /** + * Ancillary spend at the time of transaction, but not commissionable. + */ + ancillarySpend?: number + /** + * Brand of items purchased. If there are multiple items with different brands, one brand must be designated for the order. + */ + brand?: string + /** + * Identifier of brand of items purchased. If there are multiple items with different brands, one brand must be designated for the order. + */ + brandId?: string + /** + * Identifies the business unit the customer purchased through. If there are multiple items with different business units, one business unit must be designated for the order. + */ + businessUnit?: string + /** + * Marketing campaign id. + */ + campaignId?: string + /** + * Marketing campaign name. + */ + campaignName?: string + /** + * Category of items purchased. If there are multiple items with different categories, one category must be designated for the order. + */ + category?: string + /** + * Class of item. + */ + class?: string + /** + * Confirmation Number + */ + confirmationNumber?: number + /** + * The value (amount of discount) of the coupon. This should be the number linked to the "coupon_type" parameter + */ + couponDiscount?: number + /** + * The type of coupon used in the order. This should be the number linked to the "coupon_discount" parameter. + */ + couponType?: string + /** + * The customer's country. ISO 3166-1 alpha 2 country code, eg. US, UK, AU, FR. + */ + customerCountry?: string + /** + * Advertiser-specific customer segment definition. + */ + customerSegment?: string + /** + * The status of the customer, e.g. new, returning. + */ + customerStatus?: string + /** + * Indicates the type of individual making the purchase as someone representing a group. + */ + customerType?: string + /** + * The delivery method used for the order. + */ + delivery?: string + /** + * Description of the product. + */ + description?: string + /** + * Duration in days. + */ + duration?: number + /** + * End date and time of the order, in ISO 8601 format. + */ + endDateTime?: string + /** + * Product genre. If there are multiple items with different genres, one genre must be designated for the order. + */ + genre?: string + /** + * Id for the item. (Simple Actions Only). + */ + itemId?: string + /** + * Advertiser assigned item name. + */ + itemName?: string + /** + * Advertiser assigned item type. + */ + itemType?: string + /** + * Advertiser assigned general demographic. + */ + lifestage?: string + /** + * Identifies the customer location if different from customerCountry. + */ + location?: string + /** + * Loyalty points earned on the transaction. + */ + loyaltyEarned?: number + /** + * Indicates whether this order coincided with the consumer joining the loyalty program. + */ + loyaltyFirstTimeSignup?: string + /** + * Indicates the level of the customer's loyalty status. + */ + loyaltyLevel?: string + /** + * Loyalty points used during the transaction. + */ + loyaltyRedeemed?: number + /** + * Indicates if the customer is a loyalty member. + */ + loyaltyStatus?: string + /** + * Margin on total order. Can be a dollar value or a custom indicator. + */ + margin?: string + /** + * Advertiser-defined marketing channel assigned to this transaction. + */ + marketingChannel?: string + /** + * Indicates if the purchase has a no cancellation policy. "Yes" means there is a "no cancellation" policy, "no" means there is no policy. + */ + noCancellation?: string + /** + * Subtotal for order. + */ + orderSubtotal?: number + /** + * Method of payment. + */ + paymentMethod?: string + /** + * Model of payment used; advertiser-specific. + */ + paymentModel?: string + /** + * Device platform customer is using. + */ + platformId?: string + /** + * Point of sale for the transaction. + */ + pointOfSale?: string + /** + * Indicates if the purchase was made prior to the item becoming available. + */ + preorder?: string + /** + * Indicates if the payment was made in advance of the item's consumption. + */ + prepaid?: string + /** + * Promotion applied. If multiple, must be comma-separated. + */ + promotion?: string + /** + * The numeric value associated with the promotion. + */ + promotionAmount?: number + /** + * Threshold needed to qualify for the promotion. + */ + promotionConditionThreshold?: number + /** + * Type of conditions applied to the promotion. + */ + promotionConditionType?: string + /** + * End date of the promotion, in ISO 8601 format. + */ + promotionEnds?: string + /** + * Start date of the promotion, in ISO 8601 format. + */ + promotionStarts?: string + /** + * Category of promotion. + */ + promotionType?: string + /** + * Quantity for a given SKU. (Simple Actions Only). + */ + quantity?: number + /** + * Rating of the product. + */ + rating?: string + /** + * Classification of service offered. + */ + serviceType?: string + /** + * Start of item duration (e.g., check-out or departure date/time). Must be in ISO 8601 format. + */ + startDateTime?: string + /** + * Cost of subscription fee featured when signing up for free trial. + */ + subscriptionFee?: number + /** + * Product duration. + */ + subscriptionLength?: string + /** + * Total tax for the order. + */ + taxAmount?: number + /** + * Type of tax assessed. + */ + taxType?: string + /** + * Indicates if someone converted from a trial to a subscription. + */ + upsell?: string + } + /** + * This field is used to pass additional parameters specific to the travel vertical. + */ + travelVerticals?: { + /** + * Date the booking was made. + */ + bookingDate?: string + /** + * Booking status at the time of tag firing. + */ + bookingStatus?: string + /** + * Value of booking after taxes. + */ + bookingValuePostTax?: number + /** + * Value of booking before taxes. + */ + bookingValuePreTax?: number + /** + * Other items added to the reservation beyond the vehicle itself (e.g. "insurance", "GPS", "Car Seat"). + */ + carOptions?: string + /** + * Class of item (flight, hotel, car, or cruise specific classes). + */ + class?: string + /** + * Type of cruise (Alaskan, Caribbean, etc...). + */ + cruiseType?: string + /** + * Customer service destination city name (New York City, Boston, Atlanta, etc...). If destinationCity is provided, destinationState must also be provided. If there is no Origin/Destination combo, but needs to indicate a location of service (network service, event) use 'destination' set of parameters. + */ + destinationCity?: string + /** + * Customer service destination country code, per ISO 3166-1 alpha 3 country code (USA, GBR, SWE, etc...). + */ + destinationCountry?: string + /** + * Customer service destination state/province code. ISO 3166-2 country subdivision standards. e.g. US-NY, US-CA, US-FL, US-TX + */ + destinationState?: string + /** + * Indicates whether the travel is domestic (Yes) or international (No). + */ + domestic?: string + /** + * Destination location IATA code. 3 letter IATA code. + */ + dropoffIata?: string + /** + * Advertiser ID for destination location. + */ + dropoffId?: string + /** + * Type of flight fare (e.g. gotta get away). + */ + flightFareType?: string + /** + * Other items added to the reservation (e.g. Wi-Fi). + */ + flightOptions?: string + /** + * Type of flight (e.g. direct, layover, overnight). + */ + flightType?: string + /** + * Flyer miles earned from this flight. + */ + flyerMiles?: number + /** + * Number of guests. + */ + guests?: number + /** + * IATA code (3-letter); If using for a multi-stop flight each city in a flight can be provided in a comma-separated list. + */ + iata?: string + /** + * Booking itinerary ID. + */ + itineraryId?: string + /** + * Minimum stay duration required in days. + */ + minimumStayDuration?: number + /** + * Customer service origin city name (New York City, Ottawa, Los Angeles, etc...). If originCity is provided, originState is also provided + */ + originCity?: string + /** + * Customer service origin country code per ISO 3166-1 alpha 3 country code (USA, GBR, SWE, etc...). + */ + originCountry?: string + /** + * Customer service origin state/province code per ISO 3166-2 country subdivision standards (Alaska would be "or_state=US-AK", Bangkok would be "or_state=TH-10"). + */ + originState?: string + /** + * Amount paid at booking after taxes. + */ + paidAtBookingPostTax?: number + /** + * Amount paid at booking before taxes. + */ + paidAtBookingPreTax?: number + /** + * Origin location IATA code. + */ + pickupIata?: string + /** + * Advertiser ID for origin location. + */ + pickupId?: string + /** + * Departure port city (for cruises). + */ + port?: string + /** + * Room type booked. If using the same values listed for "class" parameter, use that parameter instead. + */ + roomType?: string + /** + * Number of rooms booked. + */ + rooms?: number + /** + * Name of the cruise ship. + */ + shipName?: string + /** + * Type of travel being booked. If you want access to standardized benchmark reporting, you must pass a value from the following list. + */ + travelType?: string + } + /** + * This field is used to pass additional parameters specific to the finance vertical. + */ + financeVerticals?: { + /** + * Amount of the annual fee. + */ + annualFee?: number + /** + * Identifies the status of the application at the time the transaction is sent to CJ. + */ + applicationStatus?: string + /** + * APR at time of application approval. + */ + apr?: number + /** + * APR for transfers. + */ + aprTransfer?: number + /** + * If transfer APR is only for a certain period of time, pass the number of months here. + */ + aprTransferTime?: number + /** + * Category of the card. + */ + cardCategory?: string + /** + * Amount of the fee associated with the cash advance. + */ + cashAdvanceFee?: number + /** + * Contract length, in months. + */ + contractLength?: number + /** + * Advertiser-specific contract description. + */ + contractType?: string + /** + * Indicates if the customer received a credit report and if it was purchased. + */ + creditReport?: string + /** + * Amount of credit extended through product. + */ + creditLine?: number + /** + * Minimum credit tier required for product approval. (300-579=Very Poor, 580-669=Fair, 670-739=Good,740-799=Very Good, 800-850=Exceptional). + */ + creditQuality?: string + /** + * Indicates the amount of funding added to the account at the time of transaction. + */ + fundedAmount?: number + /** + * Currency of the funding provided for the new account. + */ + fundedCurrency?: number + /** + * The introductory APR amount. (If the intro APR is not different than overall APR, use the "APR" parameter). + */ + introductoryApr?: number + /** + * The number of months the intro APR applies for. + */ + introductoryAprTime?: number + /** + * Value of the minimum cash balance requirement for the account. + */ + minimumBalance?: number + /** + * Indicates the value if a minimum deposit is required. + */ + minimumDeposit?: number + /** + * Indicates if the applicant was pre-qualified for the card. + */ + prequalify?: string + /** + * The transfer fee amount (i.e. for a credit card). + */ + transferFee?: number + } + /** + * This field is used to pass additional parameters specific to the network services vertical. + */ + networkServicesVerticals?: { + /** + * Amount of the annual fee. + */ + annualFee?: number + /** + * Identifies the status of the application at the time the transaction is sent to CJ. + */ + applicationStatus?: string + /** + * Contract length, in months. + */ + contractLength?: number + /** + * Advertiser-specific contract description. + */ + contractType?: string + } +} diff --git a/packages/browser-destinations/destinations/cj/src/order/hashing-utils.ts b/packages/browser-destinations/destinations/cj/src/order/hashing-utils.ts new file mode 100644 index 00000000000..7614c8ba4ee --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/order/hashing-utils.ts @@ -0,0 +1,141 @@ +/** + * Utility module for hashing values using various encryption methods and digest types. + * It includes functionality to check if a value is already hashed and to process hashing with optional cleaning. + */ +import btoa from 'btoa-lite' + +export type EncryptionMethod = 'sha1' | 'sha224' | 'sha256' | 'sha384' | 'sha512' +export const hashConfigs: { + [key in EncryptionMethod]: { lengthHex: number; lengthBase64: number } +} = { + sha1: { lengthHex: 40, lengthBase64: 28 }, + sha224: { lengthHex: 56, lengthBase64: 40 }, + sha256: { lengthHex: 64, lengthBase64: 44 }, + sha384: { lengthHex: 96, lengthBase64: 64 }, + sha512: { lengthHex: 128, lengthBase64: 88 } +} + +export const DigestTypes = ['hex', 'base64'] as const +export type DigestType = typeof DigestTypes[number] + +type CleaningFunction = (value: string) => string + +class SmartHashing { + private preHashed: boolean + + /** + * Creates an instance of SmartHashing. + * @param encryptionMethod - The method of encryption to be used. + * @param digest - The type of digest to be used. + */ + constructor(public encryptionMethod: EncryptionMethod = 'sha256', public digest: DigestType = 'hex') { + this.preHashed = false + } + + isAlreadyHashed(value: string): boolean { + const config = hashConfigs[this.encryptionMethod] + if (!config) throw new Error(`Unsupported encryption method: ${this.encryptionMethod}`) + + let regex: RegExp + switch (this.digest) { + case 'hex': + regex = new RegExp(`^[a-f0-9]{${config.lengthHex}}$`, 'i') + this.preHashed = value.length === config.lengthHex && regex.test(value) + break + case 'base64': + regex = new RegExp(`^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$`) + this.preHashed = value.length === config.lengthBase64 && regex.test(value) + break + default: + throw new Error(`Unsupported digest type: ${this.digest}`) + } + + return this.preHashed + } + + async hash(value: string): Promise { + if (value.trim() === '') { + throw new Error('Cannot hash an empty string') + } + + if (this.preHashed) { + return value + } + + const hash = await createHash(value, this.encryptionMethod, this.digest) + + return hash + } +} + +export async function createHash( + value: string, + encryptionMethod: EncryptionMethod = 'sha256', + digest: DigestType = 'hex' +): Promise { + const algo = encryptionMethod.toUpperCase().replace(/^SHA(\d+)$/, 'SHA-$1') + const encoded = new TextEncoder().encode(value); + const hashBuffer = await window.crypto.subtle.digest(algo, encoded) + const hashArray = new Uint8Array(hashBuffer) + + return digest === 'hex' ? toHex(hashArray) : toBase64(hashArray) +} + +function toHex(bytes: Uint8Array): string { + return Array.from(bytes) + .map(b => b.toString(16).padStart(2, '0')) + .join('') +} + +function toBase64(bytes: Uint8Array): string { + const binary = String.fromCharCode(...bytes) + return btoa(binary) +} + +/** + * Processes the hashing of a given value based on the provided encryption method, digest type, features, and optional cleaning function. + * + * @param value - The string value to be hashed. + * @param encryptionMethod - The method of encryption to be used. + * @param digest - The type of digest to be used. + * @param cleaningFunction - An optional function to clean the value before hashing. + * @returns The hashed value or the original value if it is already hashed. + */ + +export async function processHashing( + value: string, + encryptionMethod: EncryptionMethod, + digest: DigestType, + cleaningFunction?: CleaningFunction +): Promise { + if (value.trim() === '') { + return '' + } + + const smartHashing = new SmartHashing(encryptionMethod, digest) + + if (smartHashing.isAlreadyHashed(value)) { + return value + } + + if (cleaningFunction) { + value = cleaningFunction(value) + } + return smartHashing.hash(value) +} + +export async function smartHash(value: string, normalizeFunction?: (value: string) => string): Promise { + return await processHashing(value, 'sha256', 'hex', normalizeFunction) +} + +function normalize(value: string, allowedChars: RegExp, trim = true): string { + let normalized = value.toLowerCase().replace(allowedChars, '') + if (trim) normalized = normalized.trim() + return normalized +} + +const emailAllowed = /[^a-z0-9.@+-]/g + +export function normalizeEmail(email: string): string { + return normalize(email, emailAllowed) +} diff --git a/packages/browser-destinations/destinations/cj/src/order/index.ts b/packages/browser-destinations/destinations/cj/src/order/index.ts new file mode 100644 index 00000000000..2805974044e --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/order/index.ts @@ -0,0 +1,90 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import type { CJ, SimpleOrder, AdvancedOrder } from '../types' +import { send } from '../utils' +import { getCookieValue, setOrderJSON } from './utils' +import { smartHash, normalizeEmail } from './hashing-utils' +import { orderFields } from './order-fields' + +const action: BrowserActionDefinition = { + title: 'Order', + description: 'Send order data to CJ.', + defaultSubscription: 'event = "Order Completed"', + platform: 'web', + fields: orderFields, + perform: async (cj, { payload, settings }) => { + const { + verticalType, + userId, + enterpriseId, + pageType, + emailHash, + orderId, + actionTrackerId, + currency, + amount, + discount, + coupon, + cjeventOrderCookieName, + items, + allVerticals, + travelVerticals, + financeVerticals, + networkServicesVerticals + } = payload + + const cjeventOrder: string | null = getCookieValue(cjeventOrderCookieName) + + if (!cjeventOrder) { + console.warn( + `Segment CJ Actions Destination: Cookie ${cjeventOrderCookieName} not found. Please ensure the cookie is set before calling this action.` + ) + } + + const actionTrackerIdFromSettings = settings.actionTrackerId + + if (!actionTrackerId && !actionTrackerIdFromSettings) { + console.warn( + 'Segment CJ Actions Destination: Missing actionTrackerId. This can be set as a Setting or as an Action field value.' + ) + } + + const order: SimpleOrder | AdvancedOrder = { + trackingSource: 'Segment', + userId, + enterpriseId, + pageType, + emailHash: emailHash ? await smartHash(emailHash, normalizeEmail) : undefined, + orderId, + actionTrackerId: actionTrackerId ?? actionTrackerIdFromSettings ?? '', + currency, + amount, + discount, + coupon, + cjeventOrder: cjeventOrder ?? '', + items, + ...allVerticals, + ...verticalType === 'travel' ? travelVerticals : {}, + ...verticalType === 'finance' ? financeVerticals: {}, + ...verticalType === 'network' ? networkServicesVerticals : {} + } + + if ('customerCountry' in order && typeof order.customerCountry === 'string') { + order.customerCountry = order.customerCountry.split('-').pop() || undefined + } + + setOrderJSON(cj, order) + const { tagId } = settings + send(tagId) + .then(() => { + cj.sitePage = undefined + cj.order = undefined + }) + .catch((err) => { + console.warn(err) + }) + } +} + +export default action diff --git a/packages/browser-destinations/destinations/cj/src/order/order-fields.ts b/packages/browser-destinations/destinations/cj/src/order/order-fields.ts new file mode 100644 index 00000000000..a1fb2c60bf6 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/order/order-fields.ts @@ -0,0 +1,1117 @@ +import { InputField } from '@segment/actions-core' + +export const allVerticals: InputField = { + label: 'All Vertical Parameters', + description: + "This field is used to pass additional parameters specific to the vertical. All vertical parameters listed below can be utilized regardless of the account's vertical.", + type: 'object', + required: false, + defaultObjectUI: 'keyvalue', + properties: { + ancillarySpend: { + label: 'Ancillary Spend', + description: 'Ancillary spend at the time of transaction, but not commissionable.', + type: 'number' + }, + brand: { + label: 'Brand', + description: + 'Brand of items purchased. If there are multiple items with different brands, one brand must be designated for the order.', + type: 'string' + }, + brandId: { + label: 'Brand ID', + description: + 'Identifier of brand of items purchased. If there are multiple items with different brands, one brand must be designated for the order.', + type: 'string' + }, + businessUnit: { + label: 'Business Unit', + description: + 'Identifies the business unit the customer purchased through. If there are multiple items with different business units, one business unit must be designated for the order.', + type: 'string' + }, + campaignId: { + label: 'Campaign ID', + description: 'Marketing campaign id.', + type: 'string' + }, + campaignName: { + label: 'Campaign Name', + description: 'Marketing campaign name.', + type: 'string' + }, + category: { + label: 'Category', + description: + 'Category of items purchased. If there are multiple items with different categories, one category must be designated for the order.', + type: 'string' + }, + class: { + label: 'Class', + description: 'Class of item.', + type: 'string' + }, + confirmationNumber: { + label: 'Confirmation Number', + description: 'Confirmation Number', + type: 'number' + }, + couponDiscount: { + label: 'Coupon Discount', + description: + 'The value (amount of discount) of the coupon. This should be the number linked to the "coupon_type" parameter', + type: 'number' + }, + couponType: { + label: 'Coupon Type', + description: + 'The type of coupon used in the order. This should be the number linked to the "coupon_discount" parameter.', + type: 'string', + choices: [ + { label: 'Percent', value: 'percent' }, + { label: 'Dollars', value: 'dollars' }, + { label: 'Added Value', value: 'added_value' } + ] + }, + customerCountry: { + label: 'Customer Country', + description: "The customer's country. ISO 3166-1 alpha 2 country code, eg. US, UK, AU, FR.", + type: 'string' + }, + customerSegment: { + label: 'Customer Segment', + description: 'Advertiser-specific customer segment definition.', + type: 'string' + }, + customerStatus: { + label: 'Customer Status', + description: 'The status of the customer, e.g. new, returning.', + type: 'string', + choices: [ + { label: 'New', value: 'New' }, + { label: 'Lapsed', value: 'Lapsed' }, + { label: 'Return', value: 'Return' } + ] + }, + customerType: { + label: 'Customer Type', + description: 'Indicates the type of individual making the purchase as someone representing a group.', + type: 'string' + }, + delivery: { + label: 'Delivery', + description: 'The delivery method used for the order.', + type: 'string', + choices: [ + { label: 'In-Store', value: 'IN_STORE' }, + { label: 'Pick-up', value: 'PICK_UP' }, + { label: 'Recurring', value: 'RECURRING' }, + { label: 'Standard', value: 'STANDARD' }, + { label: 'Next day', value: 'NEXT_DAY' }, + { label: 'Digital', value: 'DIGITAL' }, + { label: 'Express', value: 'EXPRESS' } + ] + }, + description: { + label: 'Description', + description: 'Description of the product.', + type: 'string' + }, + duration: { + label: 'Duration', + description: 'Duration in days.', + type: 'number' + }, + endDateTime: { + label: 'End Date Time', + description: 'End date and time of the order, in ISO 8601 format.', + type: 'string', + format: 'date-time' + }, + genre: { + label: 'Genre', + description: + 'Product genre. If there are multiple items with different genres, one genre must be designated for the order.', + type: 'string' + }, + itemId: { + label: 'Item ID', + description: 'Id for the item. (Simple Actions Only).', + type: 'string' + }, + itemName: { + label: 'Item Name', + description: 'Advertiser assigned item name.', + type: 'string' + }, + itemType: { + label: 'Item Type', + description: 'Advertiser assigned item type.', + type: 'string' + }, + lifestage: { + label: 'Lifestage', + description: 'Advertiser assigned general demographic.', + type: 'string' + }, + location: { + label: 'Location', + description: 'Identifies the customer location if different from customerCountry.', + type: 'string' + }, + loyaltyEarned: { + label: 'Loyalty Earned', + description: 'Loyalty points earned on the transaction.', + type: 'number' + }, + loyaltyFirstTimeSignup: { + label: 'Loyalty First Time Signup', + description: 'Indicates whether this order coincided with the consumer joining the loyalty program.', + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + }, + loyaltyLevel: { + label: 'Loyalty Level', + description: "Indicates the level of the customer's loyalty status.", + type: 'string' + }, + loyaltyRedeemed: { + label: 'Loyalty Redeemed', + description: 'Loyalty points used during the transaction.', + type: 'number' + }, + loyaltyStatus: { + label: 'Loyalty Status', + description: 'Indicates if the customer is a loyalty member.', + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + }, + margin: { + label: 'Margin', + description: 'Margin on total order. Can be a dollar value or a custom indicator.', + type: 'string' + }, + marketingChannel: { + label: 'Marketing Channel', + description: 'Advertiser-defined marketing channel assigned to this transaction.', + type: 'string', + choices: [ + { label: 'Affiliate', value: 'affiliate' }, + { label: 'Display', value: 'display' }, + { label: 'Social', value: 'social' }, + { label: 'Search', value: 'search' }, + { label: 'Email', value: 'email' }, + { label: 'Direct Navigation', value: 'direct navigation' } + ] + }, + noCancellation: { + label: 'No Cancellation', + description: + 'Indicates if the purchase has a no cancellation policy. "Yes" means there is a "no cancellation" policy, "no" means there is no policy.', + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + }, + orderSubtotal: { + label: 'Order Subtotal', + description: 'Subtotal for order.', + type: 'number' + }, + paymentMethod: { + label: 'Payment Method', + description: 'Method of payment.', + type: 'string', + choices: [ + { label: 'Credit/Debit Card', value: 'credit_debit_card' }, + { label: 'Direct Debit', value: 'direct_debit' }, + { label: 'EFTPOS', value: 'EFTPOS' }, + { label: 'Online Payments', value: 'online_payments' }, + { label: 'Cash', value: 'cash' }, + { label: 'Check', value: 'check' }, + { label: 'Money Order', value: 'money_order' }, + { label: 'Gift Card/Voucher', value: 'gift_card_voucher' }, + { label: 'Digital Currency', value: 'digital_currency' } + ] + }, + paymentModel: { + label: 'Payment Model', + description: 'Model of payment used; advertiser-specific.', + type: 'string' + }, + platformId: { + label: 'Platform ID', + description: 'Device platform customer is using.', + type: 'string' + }, + pointOfSale: { + label: 'Point of Sale', + description: 'Point of sale for the transaction.', + type: 'string', + choices: [ + { label: 'Amazon', value: 'AMAZON' }, + { label: 'Call Center', value: 'CALL_CENTER' }, + { label: 'Car Rental', value: 'CAR_RENTAL' }, + { label: 'Catalog', value: 'CATALOG' }, + { label: 'Hotel Location', value: 'HOTEL_LOCATION' }, + { label: 'Internet', value: 'INTERNET' }, + { label: 'In-App', value: 'IN_APP' }, + { label: 'Outlet', value: 'OUTLET' }, + { label: 'Retail Store', value: 'RETAIL_STORE' } + ] + }, + preorder: { + label: 'Preorder', + description: 'Indicates if the purchase was made prior to the item becoming available.', + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + }, + prepaid: { + label: 'Prepaid', + description: "Indicates if the payment was made in advance of the item's consumption.", + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + }, + promotion: { + label: 'Promotion', + description: 'Promotion applied. If multiple, must be comma-separated.', + type: 'string' + }, + promotionAmount: { + label: 'Promotion Amount', + description: 'The numeric value associated with the promotion.', + type: 'number' + }, + promotionConditionThreshold: { + label: 'Promotion Condition Threshold', + description: 'Threshold needed to qualify for the promotion.', + type: 'number' + }, + promotionConditionType: { + label: 'Promotion Condition Type', + description: 'Type of conditions applied to the promotion.', + type: 'string', + choices: [ + { label: 'Brand Card Signup Specific', value: 'BRAND_CARD_SIGNUP_SPECIFIC' }, + { label: 'Brand Card Specific', value: 'BRAND_CARD_SPECIFIC' }, + { label: 'Location Specific', value: 'LOCATION_SPECIFIC' }, + { label: 'Membership Required', value: 'MEMBERSHIP_REQUIRED' }, + { label: 'Loyalty Required', value: 'LOYALTY_REQUIRED' }, + { label: 'Email Signup Required', value: 'EMAIL_SIGNUP_REQUIRED' }, + { label: 'New Customer Specific', value: 'NEW_CUSTOMER_SPECIFIC' }, + { label: 'Product Specific', value: 'PRODUCT_SPECIFIC' }, + { label: 'Point of Sale Specific', value: 'POINT_OF_SALE_SPECIFIC' } + ] + }, + promotionEnds: { + label: 'Promotion Ends', + description: 'End date of the promotion, in ISO 8601 format.', + type: 'string', + format: 'date-time' + }, + promotionStarts: { + label: 'Promotion Starts', + description: 'Start date of the promotion, in ISO 8601 format.', + type: 'string', + format: 'date-time' + }, + promotionType: { + label: 'Promotion Type', + description: 'Category of promotion.', + type: 'string', + choices: [ + { label: 'BOGO', value: 'BOGO' }, + { label: 'Amount Off', value: 'AMOUNT_OFF' }, + { label: 'Free Gift', value: 'FREE_GIFT' }, + { label: 'Free Shipping', value: 'FREE_SHIP' }, + { label: 'Introductory Offer', value: 'INTRODUCTORY_OFFER' }, + { label: 'Percent Off', value: 'PERCENT_OFF' }, + { label: 'Coupon', value: 'COUPON' } + ] + }, + quantity: { + label: 'Quantity', + description: 'Quantity for a given SKU. (Simple Actions Only).', + type: 'integer' + }, + rating: { + label: 'Rating', + description: 'Rating of the product.', + type: 'string' + }, + serviceType: { + label: 'Service Type', + description: 'Classification of service offered.', + type: 'string', + choices: [ + { label: 'Cable', value: 'cable' }, + { label: 'Checking Internet', value: 'checking_internet' }, + { label: 'Credit Card', value: 'credit_card' }, + { label: 'Identity', value: 'identity' }, + { label: 'Insurance', value: 'insurance' }, + { label: 'Investment', value: 'investment' }, + { label: 'Loan', value: 'loan' }, + { label: 'Payment', value: 'payment' }, + { label: 'Phone', value: 'phone' }, + { label: 'Prepaid Debit', value: 'prepaid_debit' }, + { label: 'Savings', value: 'savings' }, + { label: 'Tax', value: 'tax' }, + { label: 'TV Satellite', value: 'tv_sat' }, + { label: 'TV Streaming', value: 'tv_stream' }, + { label: 'Wireless', value: 'wireless' }, + { label: 'Wireless Business', value: 'wireless_bus' }, + { label: 'Wireless Family', value: 'wireless_fam' }, + { label: 'Wireless Individual', value: 'wireless_ind' } + ] + }, + startDateTime: { + label: 'Start Date Time', + description: 'Start of item duration (e.g., check-out or departure date/time). Must be in ISO 8601 format.', + type: 'string', + format: 'date-time' + }, + subscriptionFee: { + label: 'Subscription Fee', + description: 'Cost of subscription fee featured when signing up for free trial.', + type: 'number' + }, + subscriptionLength: { + label: 'Subscription Length', + description: 'Product duration.', + type: 'string' + }, + taxAmount: { + label: 'Tax Amount', + description: 'Total tax for the order.', + type: 'number' + }, + taxType: { + label: 'Tax Type', + description: 'Type of tax assessed.', + type: 'string', + choices: [ + { label: 'Administrative', value: 'ADMINISTRATIVE' }, + { label: 'Carrier', value: 'CARRIER' }, + { label: 'Delivery', value: 'DELIVERY' }, + { label: 'Federal Universal Service', value: 'FEDERAL_UNIVERSAL_SERVICE' }, + { label: 'Local', value: 'LOCAL' }, + { label: 'Regulatory Cost Recovery', value: 'REGULATORY_COST_RECOVERY' }, + { label: 'Room', value: 'ROOM' }, + { label: 'Segment', value: 'SEGMENT' }, + { label: 'State', value: 'STATE' }, + { label: 'Tourist', value: 'TOURIST' }, + { label: 'V911 Service', value: 'V911_SERVICE' } + ] + }, + upsell: { + label: 'Upsell', + description: 'Indicates if someone converted from a trial to a subscription.', + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + } + }, + default: { + brand: { '@path': '$.properties.brand' }, + brandId: { '@path': '$.properties.brand_id' }, + businessUnit: { '@path': '$.properties.business_unit' }, + campaignId: { '@path': '$.properties.campaign_id' }, + campaignName: { '@path': '$.properties.campaign_name' }, + category: { '@path': '$.properties.category' }, + class: { '@path': '$.properties.class' }, + confirmationNumber: { '@path': '$.properties.confirmation_number' }, + couponDiscount: { '@path': '$.properties.coupon_discount' }, + couponType: { '@path': '$.properties.coupon_type' }, + customerCountry: { '@path': '$.context.locale' }, + customerSegment: { '@path': '$.properties.customer_segment' }, + customerStatus: { '@path': '$.properties.customer_status' }, + customerType: { '@path': '$.properties.customer_type' }, + delivery: { '@path': '$.properties.delivery' }, + description: { '@path': '$.properties.description' }, + duration: { '@path': '$.properties.duration' }, + endDateTime: { '@path': '$.properties.end_date_time' }, + genre: { '@path': '$.properties.genre' }, + itemId: { '@path': '$.properties.item_id' }, + itemName: { '@path': '$.properties.item_name' }, + itemType: { '@path': '$.properties.item_type' }, + lifestage: { '@path': '$.properties.lifestage' }, + location: { '@path': '$.properties.location' }, + loyaltyEarned: { '@path': '$.properties.loyalty_earned' }, + loyaltyFirstTimeSignup: { '@path': '$.properties.loyalty_first_time_signup' }, + loyaltyLevel: { '@path': '$.properties.loyalty_level' }, + loyaltyRedeemed: { '@path': '$.properties.loyalty_redeemed' }, + loyaltyStatus: { '@path': '$.properties.loyalty_status' }, + margin: { '@path': '$.properties.margin' }, + marketingChannel: { '@path': '$.properties.marketing_channel' }, + noCancellation: { '@path': '$.properties.no_cancellation' }, + orderSubtotal: { '@path': '$.properties.order_subtotal' }, + paymentMethod: { '@path': '$.properties.payment_method' }, + paymentModel: { '@path': '$.properties.payment_model' }, + platformId: { '@path': '$.properties.platform_id' }, + pointOfSale: { '@path': '$.properties.point_of_sale' }, + preorder: { '@path': '$.properties.preorder' }, + prepaid: { '@path': '$.properties.prepaid' }, + promotion: { '@path': '$.properties.promotion' }, + promotionAmount: { '@path': '$.properties.promotion_amount' }, + promotionConditionThreshold: { '@path': '$.properties.promotion_condition_threshold' }, + promotionConditionType: { '@path': '$.properties.promotion_condition_type' }, + promotionEnds: { '@path': '$.properties.promotion_ends' }, + promotionStarts: { '@path': '$.properties.promotion_starts' }, + promotionType: { '@path': '$.properties.promotion_type' }, + quantity: { '@path': '$.properties.quantity' }, + rating: { '@path': '$.properties.rating' }, + serviceType: { '@path': '$.properties.service_type' }, + startDateTime: { '@path': '$.properties.start_date_time' }, + subscriptionFee: { '@path': '$.properties.subscription_fee' }, + subscriptionLength: { '@path': '$.properties.subscription_length' }, + taxAmount: { '@path': '$.properties.tax_amount' }, + taxType: { '@path': '$.properties.tax_type' }, + upsell: { '@path': '$.properties.upsell' } + } +} + +export const travelVerticals: InputField = { + label: 'Travel Vertical Parameters', + description: 'This field is used to pass additional parameters specific to the travel vertical.', + type: 'object', + defaultObjectUI: 'keyvalue', + properties: { + bookingDate: { + label: 'Booking Date', + description: 'Date the booking was made.', + type: 'string', + format: 'date-time' + }, + bookingStatus: { + label: 'Booking Status', + description: 'Booking status at the time of tag firing.', + type: 'string' + }, + bookingValuePostTax: { + label: 'Booking Value Post-Tax', + description: 'Value of booking after taxes.', + type: 'number' + }, + bookingValuePreTax: { + label: 'Booking Value Pre-Tax', + description: 'Value of booking before taxes.', + type: 'number' + }, + carOptions: { + label: 'Car Options', + description: + 'Other items added to the reservation beyond the vehicle itself (e.g. "insurance", "GPS", "Car Seat").', + type: 'string' + }, + class: { + label: 'Class', + description: 'Class of item (flight, hotel, car, or cruise specific classes).', + type: 'string', + choices: [ + { label: 'Flight - first', value: 'first' }, + { label: 'Flight - business', value: 'business' }, + { label: 'Flight - premium economy', value: 'premiumeconomy' }, + { label: 'Flight - economy', value: 'economy' }, + { label: 'Flight - basic economy', value: 'basiceconomy' }, + { label: 'Hotel - standard', value: 'standard' }, + { label: 'Hotel - deluxe', value: 'deluxe' }, + { label: 'Hotel - junior suite', value: 'junior_suite' }, + { label: 'Hotel - suite', value: 'suite' }, + { label: 'Car - economy', value: 'economy' }, + { label: 'Car - compact', value: 'compact' }, + { label: 'Car - mid size', value: 'mid_size' }, + { label: 'Car - full size', value: 'full_size' }, + { label: 'Car - premium', value: 'premium' }, + { label: 'Car - luxury', value: 'luxury' }, + { label: 'Car - mini van', value: 'mini_van' }, + { label: 'Car - convertible', value: 'convertible' }, + { label: 'Car - mid size SUV', value: 'mid_size_suv' }, + { label: 'Car - standard SUV', value: 'standard_suv' }, + { label: 'Car - full size SUV', value: 'full_size_suv' }, + { label: 'Car - full size van', value: 'full_size_van' }, + { label: 'Cruise - interior', value: 'interior' }, + { label: 'Cruise - ocean view', value: 'ocean_view' }, + { label: 'Cruise - suite', value: 'suite' }, + { label: 'Cruise - balcony', value: 'balcony' } + ] + }, + cruiseType: { + label: 'Cruise Type', + description: 'Type of cruise (Alaskan, Caribbean, etc...).', + type: 'string' + }, + destinationCity: { + label: 'Destination City', + description: + "Customer service destination city name (New York City, Boston, Atlanta, etc...). If destinationCity is provided, destinationState must also be provided. If there is no Origin/Destination combo, but needs to indicate a location of service (network service, event) use 'destination' set of parameters.", + type: 'string' + }, + destinationCountry: { + label: 'Destination Country', + description: + 'Customer service destination country code, per ISO 3166-1 alpha 3 country code (USA, GBR, SWE, etc...).', + type: 'string' + }, + destinationState: { + label: 'Destination State', + description: + 'Customer service destination state/province code. ISO 3166-2 country subdivision standards. e.g. US-NY, US-CA, US-FL, US-TX', + type: 'string' + }, + domestic: { + label: 'Domestic Travel', + description: 'Indicates whether the travel is domestic (Yes) or international (No).', + type: 'string', + choices: [ + { label: 'YES', value: 'YES' }, + { label: 'NO', value: 'NO' } + ] + }, + dropoffIata: { + label: 'Dropoff IATA', + description: 'Destination location IATA code. 3 letter IATA code.', + type: 'string' + }, + dropoffId: { + label: 'Dropoff ID', + description: 'Advertiser ID for destination location.', + type: 'string' + }, + flightFareType: { + label: 'Flight Fare Type', + description: 'Type of flight fare (e.g. gotta get away).', + type: 'string' + }, + flightOptions: { + label: 'Flight Options', + description: 'Other items added to the reservation (e.g. Wi-Fi).', + type: 'string' + }, + flightType: { + label: 'Flight Type', + description: 'Type of flight (e.g. direct, layover, overnight).', + type: 'string', + choices: [ + { label: 'Multi City', value: 'MULTI_CITY' }, + { label: 'One Way', value: 'ONE_WAY' }, + { label: 'Round Trip', value: 'ROUND_TRIP' } + ] + }, + flyerMiles: { + label: 'Flyer Miles', + description: 'Flyer miles earned from this flight.', + type: 'number' + }, + guests: { + label: 'Guests', + description: 'Number of guests.', + type: 'integer' + }, + iata: { + label: 'IATA', + description: + 'IATA code (3-letter); If using for a multi-stop flight each city in a flight can be provided in a comma-separated list.', + type: 'string' + }, + itineraryId: { + label: 'Itinerary ID', + description: 'Booking itinerary ID.', + type: 'string' + }, + minimumStayDuration: { + label: 'Minimum Stay Duration', + description: 'Minimum stay duration required in days.', + type: 'number' + }, + originCity: { + label: 'Origin City', + description: + 'Customer service origin city name (New York City, Ottawa, Los Angeles, etc...). If originCity is provided, originState is also provided', + type: 'string' + }, + originCountry: { + label: 'Origin Country', + description: + 'Customer service origin country code per ISO 3166-1 alpha 3 country code (USA, GBR, SWE, etc...).', + type: 'string' + }, + originState: { + label: 'Origin State', + description: + 'Customer service origin state/province code per ISO 3166-2 country subdivision standards (Alaska would be "or_state=US-AK", Bangkok would be "or_state=TH-10").', + type: 'string' + }, + paidAtBookingPostTax: { + label: 'Paid at Booking (Post-Tax)', + description: 'Amount paid at booking after taxes.', + type: 'number' + }, + paidAtBookingPreTax: { + label: 'Paid at Booking (Pre-Tax)', + description: 'Amount paid at booking before taxes.', + type: 'number' + }, + pickupIata: { + label: 'Pickup IATA', + description: 'Origin location IATA code.', + type: 'string' + }, + pickupId: { + label: 'Pickup ID', + description: 'Advertiser ID for origin location.', + type: 'string' + }, + port: { + label: 'Port', + description: 'Departure port city (for cruises).', + type: 'string' + }, + roomType: { + label: 'Room Type', + description: + 'Room type booked. If using the same values listed for "class" parameter, use that parameter instead.', + type: 'string' + }, + rooms: { + label: 'Rooms', + description: 'Number of rooms booked.', + type: 'integer' + }, + shipName: { + label: 'Ship Name', + description: 'Name of the cruise ship.', + type: 'string' + }, + travelType: { + label: 'Travel Type', + description: + ' Type of travel being booked. If you want access to standardized benchmark reporting, you must pass a value from the following list.', + type: 'string', + choices: [ + { label: 'Activities', value: 'ACTIVITIES' }, + { label: 'Air', value: 'AIR' }, + { label: 'Car', value: 'CAR' }, + { label: 'Cruise', value: 'CRUISE' }, + { label: 'Events', value: 'EVENTS' }, + { label: 'Hotel', value: 'HOTEL' }, + { label: 'Other', value: 'OTHER' }, + { label: 'Package', value: 'PACKAGE' }, + { label: 'Restaurants', value: 'RESTAURANTS' }, + { label: 'Travel Guides', value: 'TRAVEL_GUIDES' }, + { label: 'Vacation Rental', value: 'VACATION_RENTAL' } + ] + } + }, + default: { + bookingDate: { '@path': '$.properties.booking_date' }, + bookingStatus: { '@path': '$.properties.booking_status' }, + bookingValuePostTax: { '@path': '$.properties.booking_value_post_tax' }, + bookingValuePreTax: { '@path': '$.properties.booking_value_pre_tax' }, + carOptions: { '@path': '$.properties.car_options' }, + class: { '@path': '$.properties.class' }, + cruiseType: { '@path': '$.properties.cruise_type' }, + destinationCity: { '@path': '$.properties.destination_city' }, + destinationCountry: { '@path': '$.properties.destination_country' }, + destinationState: { '@path': '$.properties.destination_state' }, + domestic: { '@path': '$.properties.domestic' }, + dropoffIata: { '@path': '$.properties.dropoff_iata' }, + dropoffId: { '@path': '$.properties.dropoff_id' }, + flightFareType: { '@path': '$.properties.flight_fare_type' }, + flightOptions: { '@path': '$.properties.flight_options' }, + flightType: { '@path': '$.properties.flight_type' }, + flyerMiles: { '@path': '$.properties.flyer_miles' }, + guests: { '@path': '$.properties.guests' }, + iata: { '@path': '$.properties.iata' }, + itineraryId: { '@path': '$.properties.itinerary_id' }, + minimumStayDuration: { '@path': '$.properties.minimum_stay_duration' }, + originCity: { '@path': '$.properties.origin_city' }, + originCountry: { '@path': '$.properties.origin_country' }, + originState: { '@path': '$.properties.origin_state' }, + paidAtBookingPostTax: { '@path': '$.properties.paid_at_booking_post_tax' }, + paidAtBookingPreTax: { '@path': '$.properties.paid_at_booking_pre_tax' }, + pickupIata: { '@path': '$.properties.pickup_iata' }, + pickupId: { '@path': '$.properties.pickup_id' }, + port: { '@path': '$.properties.port' }, + roomType: { '@path': '$.properties.room_type' }, + rooms: { '@path': '$.properties.rooms' }, + shipName: { '@path': '$.properties.ship_name' }, + travelType: { '@path': '$.properties.travel_type' } + } +} + +export const financeVerticals: InputField = { + label: 'Finance Verticals', + description: 'This field is used to pass additional parameters specific to the finance vertical.', + type: 'object', + defaultObjectUI: 'keyvalue', + properties: { + annualFee: { + label: 'Annual Fee', + description: 'Amount of the annual fee.', + type: 'number' + }, + applicationStatus: { + label: 'Application Status', + description: 'Identifies the status of the application at the time the transaction is sent to CJ.', + type: 'string', + choices: [ + { label: 'Instant Approved', value: 'instant_approved' }, + { label: 'Instant Declined', value: 'instant_declined' }, + { label: 'Pended', value: 'pended' }, + { label: 'Approved', value: 'approved' }, + { label: 'Declined', value: 'declined' }, + { label: 'Declined Counter', value: 'declined_counter' } + ] + }, + apr: { + label: 'APR', + description: 'APR at time of application approval.', + type: 'number' + }, + aprTransfer: { + label: 'APR Transfer', + description: 'APR for transfers.', + type: 'number' + }, + aprTransferTime: { + label: 'APR Transfer Time', + description: 'If transfer APR is only for a certain period of time, pass the number of months here.', + type: 'integer' + }, + cardCategory: { + label: 'Card Category', + description: 'Category of the card.', + type: 'string', + choices: [ + { label: 'Balance Transfer Cards', value: 'BALANCE_TRANSFER_CARDS' }, + { label: 'Cash Back Reward Cards', value: 'CASH_BACK_REWARD_CARDS' }, + { label: 'Charge Cards', value: 'CHARGE_CARDS' }, + { label: 'Clicks', value: 'CLICKS' }, + { label: 'Military Affiliate', value: 'MILITARY_AFFILIATE' }, + { label: 'Other', value: 'OTHER' }, + { label: 'Reward Points Cards', value: 'REWARD_POINTS_CARDS' }, + { label: 'Credit Building Cards', value: 'CREDIT_BUILDING_CARDS' }, + { label: 'Student Cards', value: 'STUDENT_CARDS' }, + { label: 'Travel Reward Cards', value: 'TRAVEL_REWARD_CARDS' }, + { label: 'Unknown Product', value: 'UNKNOWN_PRODUCT' }, + { label: 'Low APR Cards', value: 'LOW_APR_CARDS' } + ] + }, + cashAdvanceFee: { + label: 'Cash Advance Fee', + description: 'Amount of the fee associated with the cash advance.', + type: 'number' + }, + contractLength: { + label: 'Contract Length', + description: 'Contract length, in months.', + type: 'number' + }, + contractType: { + label: 'Contract Type', + description: 'Advertiser-specific contract description.', + type: 'string' + }, + creditReport: { + label: 'Credit Report', + description: 'Indicates if the customer received a credit report and if it was purchased.', + type: 'string', + choices: [ + { label: 'Purchase', value: 'purchase' }, + { label: 'Free', value: 'free' }, + { label: 'Trial', value: 'trial' } + ] + }, + creditLine: { + label: 'Credit Line', + description: 'Amount of credit extended through product.', + type: 'number' + }, + creditQuality: { + label: 'Credit Quality', + description: + 'Minimum credit tier required for product approval. (300-579=Very Poor, 580-669=Fair, 670-739=Good,740-799=Very Good, 800-850=Exceptional).', + type: 'string', + choices: [ + { label: 'Very Poor', value: 'Very Poor' }, + { label: 'Fair', value: 'Fair' }, + { label: 'Good', value: 'Good' }, + { label: 'Very Good', value: 'Very Good' }, + { label: 'Exceptional', value: 'Exceptional' } + ] + }, + fundedAmount: { + label: 'Funded Amount', + description: 'Indicates the amount of funding added to the account at the time of transaction.', + type: 'number' + }, + fundedCurrency: { + label: 'Funded Currency', + description: 'Currency of the funding provided for the new account.', + type: 'number' + }, + introductoryApr: { + label: 'Introductory APR', + description: + 'The introductory APR amount. (If the intro APR is not different than overall APR, use the "APR" parameter).', + type: 'number' + }, + introductoryAprTime: { + label: 'Introductory APR Time', + description: 'The number of months the intro APR applies for.', + type: 'integer' + }, + minimumBalance: { + label: 'Minimum Balance', + description: 'Value of the minimum cash balance requirement for the account.', + type: 'number' + }, + minimumDeposit: { + label: 'Minimum Deposit', + description: 'Indicates the value if a minimum deposit is required.', + type: 'number' + }, + prequalify: { + label: 'Prequalify', + description: 'Indicates if the applicant was pre-qualified for the card.', + type: 'string', + choices: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' } + ] + }, + transferFee: { + label: 'Transfer Fee', + description: 'The transfer fee amount (i.e. for a credit card).', + type: 'number' + } + }, + default: { + annualFee: { '@path': '$.properties.annual_fee' }, + applicationStatus: { '@path': '$.properties.application_status' }, + apr: { '@path': '$.properties.apr' }, + aprTransfer: { '@path': '$.properties.apr_transfer' }, + aprTransferTime: { '@path': '$.properties.apr_transfer_time' }, + cardCategory: { '@path': '$.properties.card_category' }, + cashAdvanceFee: { '@path': '$.properties.cash_advance_fee' }, + contractLength: { '@path': '$.properties.contract_length' }, + contractType: { '@path': '$.properties.contract_type' }, + creditReport: { '@path': '$.properties.credit_report' }, + creditLine: { '@path': '$.properties.credit_line' }, + creditQuality: { '@path': '$.properties.credit_quality' }, + fundedAmount: { '@path': '$.properties.funded_amount' }, + fundedCurrency: { '@path': '$.properties.funded_currency' }, + introductoryApr: { '@path': '$.properties.introductory_apr' }, + introductoryAprTime: { '@path': '$.properties.introductory_apr_time' }, + minimumBalance: { '@path': '$.properties.minimum_balance' }, + minimumDeposit: { '@path': '$.properties.minimum_deposit' }, + prequalify: { '@path': '$.properties.prequalify' }, + transferFee: { '@path': '$.properties.transfer_fee' } + } +} + +export const networkServicesVerticals: InputField = { + label: 'Network Services Verticals', + description: 'This field is used to pass additional parameters specific to the network services vertical.', + type: 'object', + defaultObjectUI: 'keyvalue', + properties: { + annualFee: { + label: 'Annual Fee', + description: 'Amount of the annual fee.', + type: 'number' + }, + applicationStatus: { + label: 'Application Status', + description: 'Identifies the status of the application at the time the transaction is sent to CJ.', + type: 'string', + choices: [ + { label: 'Instant Approved', value: 'instant_approved' }, + { label: 'Instant Declined', value: 'instant_declined' }, + { label: 'Pended', value: 'pended' }, + { label: 'Approved', value: 'approved' }, + { label: 'Declined', value: 'declined' }, + { label: 'Declined Counter', value: 'declined_counter' } + ] + }, + contractLength: { + label: 'Contract Length', + description: 'Contract length, in months.', + type: 'number' + }, + contractType: { + label: 'Contract Type', + description: 'Advertiser-specific contract description.', + type: 'string' + } + }, + default: { + annualFee: { '@path': '$.properties.annual_fee' }, + applicationStatus: { '@path': '$.properties.application_status' }, + contractLength: { '@path': '$.properties.contract_length' }, + contractType: { '@path': '$.properties.contract_type' } + } +} + +export const orderFields: Record = { + verticalType: { + label: 'Vertical type', + description: 'Send additional data relating to travel, finance network services', + type: 'string', + choices: [ + {label: 'Travel Vertical', value:'travel'}, + {label: 'Network Vertical', value:'network'}, + {label: 'Finance Vertical', value:'finance'} + ] + }, + userId: { + label: 'User ID', + description: 'A unique ID assigned by you to the user.', + type: 'string', + required: false, + default: { '@path': '$.userId' } + }, + enterpriseId: { + label: 'Enterprise ID', + description: 'Your CJ Enterprise ID.', + type: 'number', + required: true + }, + pageType: { + label: 'Page Type', + description: 'Page type to be sent to CJ. Must be set to "conversionConfirmation" for order events.', + type: 'string', + choices: [ + { label: 'Conversion Confirmation', value: 'conversionConfirmation' } + ], + required: true, + default: 'conversionConfirmation', + readOnly: true + }, + emailHash: { + label: 'Email address', + description: 'Segment will ensure the email address is hashed before sending to CJ.', + type: 'string', + required: false, + default: { + '@if': { + exists: { '@path': '$.context.traits.email' }, + then: { '@path': '$.context.traits.email' }, + else: { '@path': '$.properties.email' } + } + } + }, + orderId: { + label: 'Order ID', + description: + 'The orderId is a unique identifier, such as an order identifier or invoice number, which must be populated for each order.', + type: 'string', + required: true, + default: { '@path': '$.properties.order_id' } + }, + actionTrackerId: { + label: 'Action Tracker ID', + description: + 'Required if not specified in Settings. This is a static value provided by CJ. Each account may have multiple actions and each will be referenced by a different actionTrackerId value.', + type: 'string' + }, + currency: { + label: 'Currency', + description: 'The currency of the order, e.g. USD, EUR.', + type: 'string', + required: true, + default: { '@path': '$.properties.currency' } + }, + amount: { + label: 'Amount', + description: 'The total amount of the order. This should exclude shipping or tax.', + type: 'number', + required: true, + default: { '@path': '$.properties.total' } + }, + discount: { + label: 'Discount', + description: 'The total discount applied to the order.', + type: 'number', + required: false, + default: { '@path': '$.properties.discount' } + }, + coupon: { + label: 'Coupon', + description: 'The coupon code applied to the order.', + type: 'string', + required: false, + default: { '@path': '$.properties.coupon' } + }, + cjeventOrderCookieName: { + label: 'CJ Event Order Cookie Name', + description: + 'The name of the cookie that stores the CJ Event ID. This is required whenever the advertiser uses their own cookie to store the Event ID.', + type: 'string', + required: true, + default: 'cje' + }, + items: { + label: 'Items', + description: 'The items to be sent to CJ.', + type: 'object', + multiple: true, + properties: { + unitPrice: { + label: 'Unit Price', + description: 'the price of the item before tax and discount.', + type: 'number', + required: true + }, + itemId: { + label: 'Item ID', + description: 'The item sku.', + type: 'string', + required: true + }, + quantity: { + label: 'Quantity', + description: 'The quantity of the item.', + type: 'number', + required: true + }, + discount: { + label: 'Discount', + description: 'The discount applied to the item.', + type: 'number', + required: false + } + }, + default: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + } + }, + allVerticals, + travelVerticals, + financeVerticals, + networkServicesVerticals +} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/cj/src/order/utils.ts b/packages/browser-destinations/destinations/cj/src/order/utils.ts new file mode 100644 index 00000000000..d4d30b2e7df --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/order/utils.ts @@ -0,0 +1,19 @@ +import { CJ } from '../types' + +export function getCookieValue(cookieName: string): string | null { + const name = cookieName + '=' + const decodedCookie = decodeURIComponent(document.cookie) + const cookieArray = decodedCookie.split('; ') + + for (const cookie of cookieArray) { + if (cookie.startsWith(name)) { + return cookie.substring(name.length) + } + } + + return null +} + +export function setOrderJSON(cj: CJ, orderJSON: CJ['order']) { + cj.order = orderJSON +} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/cj/src/sitePage/__tests__/index.test.ts b/packages/browser-destinations/destinations/cj/src/sitePage/__tests__/index.test.ts new file mode 100644 index 00000000000..4e7fa00c394 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/sitePage/__tests__/index.test.ts @@ -0,0 +1,124 @@ +import { Analytics, Context } from '@segment/analytics-next' +import { Subscription } from '@segment/browser-destination-runtime' +import CJDestination, { destination } from '../../index' +import { CJ } from '../../types' +import * as sendModule from '../../utils' +import * as sitePageModule from '../utils' + +describe('CJ init', () => { + const settings = { + tagId: '123456789', + actionTrackerId: '987654321' + } + + let mockCJ: CJ + let sitePageEvent: any + beforeEach(async () => { + jest.spyOn(destination, 'initialize').mockImplementation(() => { + mockCJ = {} as CJ + return Promise.resolve(mockCJ) + }) + + jest.spyOn(sendModule, 'send').mockImplementation(() => { + return Promise.resolve() + }) + + jest.spyOn(sitePageModule, 'setSitePageJSON') + + }) + + afterEach(() => { + jest.restoreAllMocks() + }) + + test('CJ pixel sitePage event', async () => { + const subscriptions: Subscription[] = [ + { + partnerAction: 'sitePage', + name: 'sitePage', + enabled: true, + subscribe: 'type = "page"', + mapping: { + userId: { '@path': '$.userId' }, + enterpriseId: 999999, + pageType: 'homepage', + referringChannel: { '@path': '$.properties.referring_channel' }, + cartSubtotal: { '@path': '$.properties.sub_total' }, + items: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + } + } + } + ] + const context = new Context({ + type: 'page', + userId: 'userId-abc123', + properties: { + referring_channel: 'Email', + sub_total: 10.99, + products: [ + { + id: '123', + quantity: 1, + price: 1, + discount: 0.5 + }, + { + id: '456', + quantity: 2, + price: 2, + discount: 0 + } + ] + } + }) + const [event] = await CJDestination({ + ...settings, + subscriptions + }) + + const sitePageJSON = { + userId: 'userId-abc123', + enterpriseId: 999999, + pageType: 'homepage', + referringChannel: 'Email', + cartSubtotal: 10.99, + items:[ + { + itemId: '123', + quantity: 1, + itemPrice: 1, + discount: 0.5 + }, + { + itemId: '456', + quantity: 2, + itemPrice: 2, + discount: 0 + } + ] + } + + sitePageEvent = event + const sendSpy = jest.spyOn(sendModule, 'send').mockResolvedValue(undefined) + await sitePageEvent.load(Context.system(), {} as Analytics) + await sitePageEvent.track?.(context) + expect(destination.initialize).toHaveBeenCalled() + expect(sitePageModule.setSitePageJSON).toHaveBeenCalled() + expect(sitePageModule.setSitePageJSON).toHaveBeenCalledWith( + expect.any(Object), + expect.objectContaining(sitePageJSON) + ) + expect(sendSpy).toHaveBeenCalledWith('123456789') + expect(mockCJ.sitePage).toBe(undefined) + expect(mockCJ.order).toBe(undefined) + }) +}) diff --git a/packages/browser-destinations/destinations/cj/src/sitePage/generated-types.ts b/packages/browser-destinations/destinations/cj/src/sitePage/generated-types.ts new file mode 100644 index 00000000000..736da254700 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/sitePage/generated-types.ts @@ -0,0 +1,45 @@ +// Generated file. DO NOT MODIFY IT BY HAND. + +export interface Payload { + /** + * A unique ID assigned by you to the user. + */ + userId?: string + /** + * Your CJ Enterprise ID. + */ + enterpriseId: number + /** + * Page type to be sent to CJ. + */ + pageType: string + /** + * The referring channel to be sent to CJ. + */ + referringChannel?: string + /** + * The cart subtotal to be sent to CJ. + */ + cartSubtotal?: number + /** + * The items to be sent to CJ. + */ + items?: { + /** + * the price of the item before tax and discount. + */ + unitPrice: number + /** + * The item sku. + */ + itemId: string + /** + * The quantity of the item. + */ + quantity: number + /** + * The discount applied to the item. + */ + discount?: number + }[] +} diff --git a/packages/browser-destinations/destinations/cj/src/sitePage/index.ts b/packages/browser-destinations/destinations/cj/src/sitePage/index.ts new file mode 100644 index 00000000000..38d9c7928cb --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/sitePage/index.ts @@ -0,0 +1,128 @@ +import type { BrowserActionDefinition } from '@segment/browser-destination-runtime/types' +import type { Settings } from '../generated-types' +import type { Payload } from './generated-types' +import { CJ } from '../types' +import { send } from '../utils' +import { setSitePageJSON } from './utils' + +const action: BrowserActionDefinition = { + title: 'Site Page', + description: 'Send site page data to CJ.', + platform: 'web', + defaultSubscription: 'type = "page"', + fields: { + userId: { + label: 'User ID', + description: 'A unique ID assigned by you to the user.', + type: 'string', + required: false, + default: { '@path': '$.userId' } + }, + enterpriseId: { + label: 'Enterprise ID', + description: 'Your CJ Enterprise ID.', + type: 'number', + required: true + }, + pageType: { + label: 'Page Type', + description: 'Page type to be sent to CJ.', + type: 'string', + choices: [ + { label: 'Account Center', value: 'accountCenter' }, + { label: 'Account Signup', value: 'accountSignup' }, + { label: 'Application Start', value: 'applicationStart' }, + { label: 'Branch Locator', value: 'branchLocator' }, + { label: 'Cart', value: 'cart' }, + { label: 'Category', value: 'category' }, + { label: 'Conversion Confirmation', value: 'conversionConfirmation' }, + { label: 'Department', value: 'department' }, + { label: 'Homepage', value: 'homepage' }, + { label: 'Information', value: 'information' }, + { label: 'Product Detail', value: 'productDetail' }, + { label: 'Property Detail', value: 'propertyDetail' }, + { label: 'Property Results', value: 'propertyResults' }, + { label: 'Search Results', value: 'searchResults' }, + { label: 'Store Locator', value: 'storeLocator' }, + { label: 'Sub Category', value: 'subCategory' } + ], + required: true + }, + referringChannel: { + label: 'Referring Channel', + description: 'The referring channel to be sent to CJ.', + type: 'string', + choices: [ + { label: 'Affiliate', value: 'Affiliate' }, + { label: 'Display', value: 'Display' }, + { label: 'Social', value: 'Social' }, + { label: 'Search', value: 'Search' }, + { label: 'Email', value: 'Email' }, + { label: 'Direct Navigation', value: 'Direct_Navigation' } + ] + }, + cartSubtotal: { + label: 'Cart Subtotal', + description: 'The cart subtotal to be sent to CJ.', + type: 'number', + required: false + }, + items: { + label: 'Items', + description: 'The items to be sent to CJ.', + type: 'object', + multiple: true, + properties: { + unitPrice: { + label: 'Unit Price', + description: 'the price of the item before tax and discount.', + type: 'number', + required: true + }, + itemId: { + label: 'Item ID', + description: 'The item sku.', + type: 'string', + required: true + }, + quantity: { + label: 'Quantity', + description: 'The quantity of the item.', + type: 'number', + required: true + }, + discount: { + label: 'Discount', + description: 'The discount applied to the item.', + type: 'number', + required: false + } + }, + default: { + '@arrayPath': [ + '$.properties.products', + { + itemPrice: { '@path': '$.price' }, + itemId: { '@path': '$.id' }, + quantity: { '@path': '$.quantity' }, + discount: { '@path': '$.discount' } + } + ] + } + } + }, + perform: (cj, { payload, settings }) => { + setSitePageJSON(cj, payload) + const { tagId } = settings + send(tagId) + .then(() => { + cj.sitePage = undefined + cj.order = undefined + }) + .catch((err) => { + console.warn(err) + }) + } +} + +export default action diff --git a/packages/browser-destinations/destinations/cj/src/sitePage/utils.ts b/packages/browser-destinations/destinations/cj/src/sitePage/utils.ts new file mode 100644 index 00000000000..502570ba622 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/sitePage/utils.ts @@ -0,0 +1,5 @@ +import { CJ } from '../types' + +export function setSitePageJSON(cj: CJ, sitePageJSON: CJ['sitePage']) { + cj.sitePage = sitePageJSON +} \ No newline at end of file diff --git a/packages/browser-destinations/destinations/cj/src/types.ts b/packages/browser-destinations/destinations/cj/src/types.ts new file mode 100644 index 00000000000..f5d6768e310 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/types.ts @@ -0,0 +1,38 @@ +export interface CJ { + sitePage?: { + enterpriseId: number + pageType: string + referringChannel?: string + cartSubtotal?: number + items?: Item[] + userId?: string + } + order?: SimpleOrder | AdvancedOrder +} + +export interface Item { + unitPrice: number + itemId: string + quantity: number + discount?: number +} + +export interface SimpleOrder { + trackingSource: 'Segment' + enterpriseId: number + pageType?: string + userId?: string + emailHash?: string + orderId: string + actionTrackerId?: string // This is required. If not provided, log a warning to the console. + currency: string + amount: number // should default to 0 if not provided + discount?: number + coupon?: string + cjeventOrder?: string //required whenever advertiser uses their own cookie to store the Event ID +} + +export interface AdvancedOrder extends SimpleOrder { + items: Item[] +} + diff --git a/packages/browser-destinations/destinations/cj/src/utils.ts b/packages/browser-destinations/destinations/cj/src/utils.ts new file mode 100644 index 00000000000..ab41b183857 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/src/utils.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ +// @ts-nocheck + +export function send(tagId): Promise { + return new Promise((resolve, reject) => { + ;(function (a, b, c, d) { + a = `//www.mczbf.com/tags/${tagId}/tag.js` + b = document + c = 'script' + d = b.createElement(c) + d.src = a + d.type = 'text/java' + c + d.async = true + d.id = 'cjapitag' + d.onload = () => resolve() + d.onerror = () => reject(new Error('JC script failed to load correctly')) + a = b.getElementsByTagName(c)[0] + a.parentNode.insertBefore(d, a) + })() + }) +} diff --git a/packages/browser-destinations/destinations/cj/tsconfig.json b/packages/browser-destinations/destinations/cj/tsconfig.json new file mode 100644 index 00000000000..c2a7897afd6 --- /dev/null +++ b/packages/browser-destinations/destinations/cj/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "rootDir": "./src", + "baseUrl": "." + }, + "include": ["src"], + "exclude": ["dist", "**/__tests__"] +}