diff --git a/src/app/analytics/ga.service.test.ts b/src/app/analytics/ga.service.test.ts index be2b09f002..5bb3a82fe7 100644 --- a/src/app/analytics/ga.service.test.ts +++ b/src/app/analytics/ga.service.test.ts @@ -74,6 +74,11 @@ beforeEach(() => { vi.clearAllMocks(); }); +function getGtagPayload(eventName: string): any { + const call = vi.mocked(globalThis.window.gtag).mock.calls.find((c) => c[0] === 'event' && c[1] === eventName); + return call?.[2]; +} + describe('Testing GA Service', () => { describe('track function', () => { it('should push event to dataLayer when called with event name and data', () => { @@ -108,14 +113,17 @@ describe('Testing GA Service', () => { describe('trackBeginCheckout function', () => { describe('Successful tracking', () => { - it('should push begin_checkout event to dataLayer', () => { + it('When a checkout starts, then a single begin_checkout event is reported to Google Analytics', () => { gaService.trackBeginCheckout(mockTrackBeginCheckoutParams); - expect(globalThis.window.dataLayer).toHaveLength(1); - expect(globalThis.window.dataLayer[0].event).toBe('begin_checkout'); + const beginCheckoutCalls = vi + .mocked(globalThis.window.gtag) + .mock.calls.filter((call) => call[1] === 'begin_checkout'); + expect(beginCheckoutCalls).toHaveLength(1); + expect(globalThis.window.dataLayer).toHaveLength(0); }); - it('should call gtag with send_to configuration', () => { + it('When a checkout starts, then the event is reported to the configured analytics destinations', () => { gaService.trackBeginCheckout(mockTrackBeginCheckoutParams); expect(globalThis.window.gtag).toHaveBeenCalledWith( @@ -127,24 +135,24 @@ describe('Testing GA Service', () => { ); }); - it('should include coupon code in the event when provided', () => { + it('When the customer applies a coupon, then the coupon is reported with the purchased item', () => { gaService.trackBeginCheckout(mockTrackBeginCheckoutParams); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.items[0]).toMatchObject({ + const payload = getGtagPayload('begin_checkout'); + expect(payload.items[0]).toMatchObject({ coupon: 'DISCOUNT20', }); }); - it('should calculate and include discount amount when coupon is applied', () => { + it('When the customer applies a coupon, then the resulting discount amount is reported', () => { gaService.trackBeginCheckout(mockTrackBeginCheckoutParams); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.items[0].discount).toBeDefined(); - expect(event.ecommerce.items[0].discount).toBeGreaterThan(0); + const payload = getGtagPayload('begin_checkout'); + expect(payload.items[0].discount).toBeDefined(); + expect(payload.items[0].discount).toBeGreaterThan(0); }); - it('should calculate total value correctly when multiple seats are purchased', () => { + it('When several seats are purchased, then the reported quantity and total value reflect all seats', () => { const paramsWithMultipleSeats = { ...mockTrackBeginCheckoutParams, seats: 5, @@ -152,19 +160,19 @@ describe('Testing GA Service', () => { gaService.trackBeginCheckout(paramsWithMultipleSeats); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.items[0].quantity).toBe(5); - expect(event.ecommerce.value).toBeGreaterThan(0); + const payload = getGtagPayload('begin_checkout'); + expect(payload.items[0].quantity).toBe(5); + expect(payload.value).toBeGreaterThan(0); }); - it('should format storage bytes into human-readable format', () => { + it('When a plan is bought, then its storage is reported in a human-readable format', () => { const bytesToStringSpy = vi.mocked(bytesToString); gaService.trackBeginCheckout(mockTrackBeginCheckoutParams); expect(bytesToStringSpy).toHaveBeenCalledWith(1099511627776); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.items[0].item_name).toContain('1TB'); + const payload = getGtagPayload('begin_checkout'); + expect(payload.items[0].item_name).toContain('1TB'); }); it('should save item data to localStorage for later use in purchase event', () => { @@ -184,14 +192,11 @@ describe('Testing GA Service', () => { }); describe('Error handling', () => { - it('should handle errors gracefully when tracking fails', () => { + it('When analytics reporting throws, then the checkout flow keeps working and the error is logged', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - const mockDataLayer = { - push: vi.fn(() => { - throw new Error('Tracking Error'); - }), - }; - globalThis.window.dataLayer = mockDataLayer as any; + globalThis.window.gtag = vi.fn(() => { + throw new Error('Tracking Error'); + }) as any; expect(() => gaService.trackBeginCheckout(mockTrackBeginCheckoutParams)).not.toThrow(); expect(consoleErrorSpy).toHaveBeenCalled(); @@ -203,7 +208,7 @@ describe('Testing GA Service', () => { describe('trackPurchase function', () => { describe('Successful tracking', () => { - it('should push purchase event to dataLayer with correct transaction and item data', () => { + it('When a purchase completes, then a single purchase event with the transaction and item data is reported', () => { vi.mocked(localStorageService.getUser).mockReturnValue({ uuid: 'user_uuid_123', email: 'test@example.com', @@ -230,29 +235,28 @@ describe('Testing GA Service', () => { gaService.trackPurchase(); - expect(globalThis.window.dataLayer).toHaveLength(1); - const event = globalThis.window.dataLayer[0] as any; + const purchaseCalls = vi.mocked(globalThis.window.gtag).mock.calls.filter((call) => call[1] === 'purchase'); + expect(purchaseCalls).toHaveLength(1); + expect(globalThis.window.dataLayer).toHaveLength(0); - expect(event).toMatchObject({ - event: 'purchase', - ecommerce: { - transaction_id: 'sub_12345', - currency: 'EUR', - value: 95.9, - items: [ - { - item_id: 'price_yearly_2tb', - item_name: '2TB Year Plan', - item_category: 'Individual', - item_variant: 'year', - price: 119.88, - quantity: 1, - item_brand: 'Internxt', - coupon: 'DISCOUNT20', - discount: 23.98, - }, - ], - }, + const payload = getGtagPayload('purchase'); + expect(payload).toMatchObject({ + transaction_id: 'sub_12345', + currency: 'EUR', + value: 95.9, + items: [ + { + item_id: 'price_yearly_2tb', + item_name: '2TB Year Plan', + item_category: 'Individual', + item_variant: 'year', + price: 119.88, + quantity: 1, + item_brand: 'Internxt', + coupon: 'DISCOUNT20', + discount: 23.98, + }, + ], }); }); @@ -275,8 +279,7 @@ describe('Testing GA Service', () => { gaService.trackPurchase(); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.transaction_id).toBe('pi_999'); + expect(getGtagPayload('purchase').transaction_id).toBe('pi_999'); }); it('should use subscription ID when payment intent is not available', () => { @@ -298,8 +301,7 @@ describe('Testing GA Service', () => { gaService.trackPurchase(); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.transaction_id).toBe('sub_888'); + expect(getGtagPayload('purchase').transaction_id).toBe('sub_888'); }); it('should fallback to user UUID when neither payment intent nor subscription ID are available', () => { @@ -321,8 +323,7 @@ describe('Testing GA Service', () => { gaService.trackPurchase(); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.transaction_id).toBe('user_fallback_uuid'); + expect(getGtagPayload('purchase').transaction_id).toBe('user_fallback_uuid'); }); it('should set user email for Enhanced Conversions when available', () => { @@ -402,9 +403,9 @@ describe('Testing GA Service', () => { gaService.trackPurchase(); - const event = globalThis.window.dataLayer[0] as any; - expect(event.ecommerce.items[0].price).toBe(119.88); - expect(event.ecommerce.value).toBe(0); + const payload = getGtagPayload('purchase'); + expect(payload.items[0].price).toBe(119.88); + expect(payload.value).toBe(0); }); it('should not track when checkout data is missing (already tracked)', () => { @@ -464,7 +465,7 @@ describe('Testing GA Service', () => { consoleWarnSpy.mockRestore(); }); - it('should handle errors gracefully when dataLayer fails', () => { + it('When analytics reporting throws during a purchase, then the flow keeps working and the error is logged', () => { const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.mocked(localStorageService.getUser).mockReturnValue({ uuid: 'user_123' } as any); vi.mocked(localStorageService.get).mockImplementation((key) => { @@ -479,15 +480,12 @@ describe('Testing GA Service', () => { }); return 'dummy'; }); - const mockDataLayer = { - push: vi.fn(() => { - throw new Error('DL Error'); - }), - }; - globalThis.window.dataLayer = mockDataLayer as any; + globalThis.window.gtag = vi.fn(() => { + throw new Error('gtag Error'); + }) as any; expect(() => gaService.trackPurchase()).not.toThrow(); - expect(mockDataLayer.push).toHaveBeenCalled(); + expect(globalThis.window.gtag).toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalled(); consoleErrorSpy.mockRestore(); diff --git a/src/app/analytics/ga.service.ts b/src/app/analytics/ga.service.ts index cc5c27773a..2a9623b178 100644 --- a/src/app/analytics/ga.service.ts +++ b/src/app/analytics/ga.service.ts @@ -114,17 +114,6 @@ function trackBeginCheckout(params: TrackBeginCheckoutParams): void { localStorageService.set(LocalStorageItem.CheckoutItemData, JSON.stringify(checkoutItemData)); - const ecommerceData = { - currency: currencyCode, - value: totalAmount, - items: [item], - }; - - globalThis.window.dataLayer.push({ - event: 'begin_checkout', - ecommerce: ecommerceData, - }); - if (globalThis.window.gtag && SEND_TO.length > 0) { globalThis.window.gtag('event', 'begin_checkout', { send_to: SEND_TO, @@ -207,19 +196,6 @@ function trackPurchase(): void { }); } - const purchaseEcommerce = { - transaction_id: transactionId, - currency: currencyCode, - value: amount, - items: [item], - coupon: couponCode ?? undefined, - }; - - globalThis.window.dataLayer.push({ - event: 'purchase', - ecommerce: purchaseEcommerce, - }); - if (SEND_TO.length > 0) { globalThis.window.gtag('event', 'purchase', { send_to: SEND_TO, diff --git a/src/app/analytics/meta.service.ts b/src/app/analytics/meta.service.ts index 9c1cc3aeb8..010bc6ac31 100644 --- a/src/app/analytics/meta.service.ts +++ b/src/app/analytics/meta.service.ts @@ -37,19 +37,23 @@ export const trackPurchase = () => { const value = Number.parseFloat(amountPaid); + const eventId = + localStorageService.get('subscriptionId') ?? + localStorageService.get('paymentIntentId') ?? + undefined; + globalThis.window.dataLayer.push({ event: 'purchaseSuccessful', - ecommerce: { - value: value, - currency: currency, - }, + ecommerce: { value, currency }, + ...(eventId && { eventId }), }); - (globalThis.window as any).fbq('track', 'Purchase', { - value: value, - currency: currency, - content_type: 'product', - }); + (globalThis.window as any).fbq( + 'track', + 'Purchase', + { value, currency, content_type: 'product' }, + ...(eventId ? [{ eventID: eventId }] : []), + ); }; export const trackCheckoutStart = (data?: { value?: number; currency?: string; content_ids?: string[] }) => { diff --git a/src/views/Signup/components/SignupBlog/SignupBlog.tsx b/src/views/Signup/components/SignupBlog/SignupBlog.tsx index 75859978d8..d4fbe71b22 100644 --- a/src/views/Signup/components/SignupBlog/SignupBlog.tsx +++ b/src/views/Signup/components/SignupBlog/SignupBlog.tsx @@ -23,7 +23,7 @@ export default function SignupBlog(): JSX.Element { <> - +