Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 63 additions & 65 deletions src/app/analytics/ga.service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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(
Expand All @@ -127,44 +135,44 @@ 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,
};

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', () => {
Expand All @@ -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();
Expand All @@ -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',
Expand All @@ -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,
},
],
});
});

Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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)', () => {
Expand Down Expand Up @@ -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) => {
Expand All @@ -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();
Expand Down
24 changes: 0 additions & 24 deletions src/app/analytics/ga.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 13 additions & 9 deletions src/app/analytics/meta.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,19 +37,23 @@

const value = Number.parseFloat(amountPaid);

const eventId =
localStorageService.get('subscriptionId') ??

Check failure on line 41 in src/app/analytics/meta.service.ts

View workflow job for this annotation

GitHub Actions / typecheck (20.x)

Argument of type '"subscriptionId"' is not assignable to parameter of type 'LocalStorageItem'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now you need localStorageService.get(LocalStorageItem.SubscriptionID);

localStorageService.get('paymentIntentId') ??

Check failure on line 42 in src/app/analytics/meta.service.ts

View workflow job for this annotation

GitHub Actions / typecheck (20.x)

Argument of type '"paymentIntentId"' is not assignable to parameter of type 'LocalStorageItem'.
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[] }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/views/Signup/components/SignupBlog/SignupBlog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function SignupBlog(): JSX.Element {
<>
<Helmet>
<link rel="canonical" href={`${envService.getVariable('hostname')}/signup-blog`} />
<script src={`https://www.googletagmanager.com/gtag/js?id=${envService.getVariable('gaBlogId')}`}></script>
<script src={`https://www.googletagmanager.com/gtag/js?id=${envService.getVariable('gaId')}`}></script>
</Helmet>

<div className="flex flex-col items-center justify-center overflow-hidden bg-white">
Expand Down
Loading