Skip to content

Commit

Permalink
Revert Tracking component and related utils
Browse files Browse the repository at this point in the history
  • Loading branch information
yuriyyakym committed Jan 29, 2025
1 parent 25943f3 commit 3c6763a
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 0 deletions.
84 changes: 84 additions & 0 deletions packages/analytics-nextjs/src/components/Tracking/Tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
'use client';

/* eslint-disable @typescript-eslint/no-use-before-define */

import { useEffect, useRef } from 'react';

import type { Analytics } from '../../Analytics';
import { ACTIONS } from '../../events';

import { getRecipientInfo } from './lib/getRecipientInfo';
import { getUrlParameters } from './lib/getUrlParameters';

export const UPLOADCARE_CDN_HOSTNAME = 'cdn.uc.assets.prezly.com';

export function Tracking({ analytics }: { analytics: Analytics }) {
const { alias, identify, track, user } = analytics;
const aliasRef = useRef(alias);
const identifyRef = useRef(identify);
const trackRef = useRef(track);
const userRef = useRef(user);

useEffect(() => {
function handleClick(event: MouseEvent) {
if (event.target instanceof HTMLElement) {
const nearestAnchor = event.target.closest('a');

if (nearestAnchor) {
const url = new URL(nearestAnchor.href);
const isExternalDomain = url.hostname !== window.location.hostname;
const isUploadcareCdn = url.hostname === UPLOADCARE_CDN_HOSTNAME;

if (isExternalDomain && !isUploadcareCdn) {
track(ACTIONS.OUTBOUND_LINK_CLICK, { href: nearestAnchor.href });
}
}
}
}

document.addEventListener('click', handleClick);

return () => document.removeEventListener('click', handleClick);
}, [track]);

useEffect(() => {
const utm = getUrlParameters('utm_');
const recipientId = utm.get('id');

if (recipientId) {
getRecipientInfo(recipientId)
.then((data) => {
identifyRef.current(data.id);
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error(error);
});
}
}, [aliasRef, identifyRef, userRef]);

useEffect(() => {
const hashParameters = window.location.hash.replace('#', '').split('-');

const id = hashParameters.pop();
const type = hashParameters.join('-');

if (id && type) {
// Auto-click assest passed in query parameters (used by campaign links)
// Pulled from https://github.com/prezly/prezly/blob/9ac32bc15760636ed47eea6fe637d245fa752d32/apps/press/resources/javascripts/prezly.js#L425-L458
const delay = type === 'image' || type === 'gallery-image' ? 500 : 0;
window.setTimeout(() => {
const targetEl =
document.getElementById(`${type}-${id}`) ||
// Fallback to data-attributes marked element
document.querySelector(`[data-type='${type}'][data-id='${id}']`);

if (targetEl) {
targetEl.click();
}
}, delay);
}
}, [trackRef]);

return null;
}
1 change: 1 addition & 0 deletions packages/analytics-nextjs/src/components/Tracking/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Tracking } from './Tracking';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import fetchMock from 'jest-fetch-mock';

import type { RecipientInfo } from '../../../types';
import { getRecipientInfo } from './getRecipientInfo';

fetchMock.enableMocks();

describe('getRecipientInfo', () => {
beforeEach(() => {
fetchMock.resetMocks();
});

it('should return recipient info', async () => {
const MOCKED_RESPONSE: RecipientInfo = { campaign_id: 123, id: 'abc', recipient_id: 'def' };

// We don't need to mock all properties of the `fetch` response for this test
// @ts-expect-error
fetchMock.mockResolvedValueOnce({
ok: true,
json: async () => MOCKED_RESPONSE,
});

const info = await getRecipientInfo('abc');
expect(info).toEqual(MOCKED_RESPONSE);
});

it('should throw an error when request fails', async () => {
// We don't need to mock all properties of the `fetch` response for this test
// @ts-expect-error
fetchMock.mockResolvedValueOnce({
ok: false,
});

await expect(getRecipientInfo('abc')).rejects.toThrow(
'Failed to fetch recipient with id: abc',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { RecipientInfo } from '../../../types';

Check failure on line 1 in packages/analytics-nextjs/src/components/Tracking/lib/getRecipientInfo.ts

View workflow job for this annotation

GitHub Actions / ESLint

packages/analytics-nextjs/src/components/Tracking/lib/getRecipientInfo.ts#L1

There should be no empty line within import group (import/order)

import { getApiUrl } from '../../../lib/getApiUrl';

Check failure on line 3 in packages/analytics-nextjs/src/components/Tracking/lib/getRecipientInfo.ts

View workflow job for this annotation

GitHub Actions / ESLint

packages/analytics-nextjs/src/components/Tracking/lib/getRecipientInfo.ts#L3

`../../../lib/getApiUrl` import should occur before type import of `../../../types` (import/order)

export async function getRecipientInfo(recipientId: string): Promise<RecipientInfo> {
const url = getApiUrl();
const response = await fetch(`${url}/recipients?id=${recipientId}`);
if (!response.ok) {
throw new Error(`Failed to fetch recipient with id: ${recipientId}`);
}

return response.json();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'jest-location-mock';

import { getUrlParameters } from './getUrlParameters';

describe('getUrlParameters', () => {
afterEach(() => {
window.location.assign('/');
});

it('should return empty Map with no search parameters', () => {
expect(getUrlParameters('asset_')).toEqual(new Map());
});

it('should return empty Map with search parameters not matching the prefix', () => {
window.location.assign('/?utm_id=abc');
expect(window.location.search).toBe('?utm_id=abc');

expect(getUrlParameters('asset_')).toEqual(new Map());
});

it('should return correct Map with search parameters matching the prefix', () => {
window.location.assign('/?asset_id=abc');
expect(window.location.search).toBe('?asset_id=abc');

const assetParams = getUrlParameters('asset_');

expect(assetParams.size).toBe(1);
expect(assetParams.get('id')).toBe('abc');

window.location.assign('/?utm_id=abc&utm_media=email');
expect(window.location.search).toBe('?utm_id=abc&utm_media=email');

const urmParams = getUrlParameters('utm_');

expect(urmParams.size).toBe(2);
expect(urmParams.get('id')).toBe('abc');
expect(urmParams.get('media')).toBe('email');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
type ParameterPrefix = 'asset_' | 'utm_';

export function getUrlParameters(prefix: ParameterPrefix): Map<string, string> {
const searchParams = new URLSearchParams(window.location.search);
const map = new Map();

searchParams.forEach((value, name) => {
if (value && value !== 'undefined' && name.startsWith(prefix)) {
map.set(name.replace(prefix, ''), value);
}
});

return map;
}
1 change: 1 addition & 0 deletions packages/analytics-nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './events';
export { Analytics } from './Analytics';
export type { PrezlyMeta } from './types';
export { TrackingPolicy } from './types';
export { Tracking } from './components/Tracking';
6 changes: 6 additions & 0 deletions packages/analytics-nextjs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ export interface Identity {
traits?: object;
}

export interface RecipientInfo {
campaign_id: number;
id: string;
recipient_id: string;
}

// Pulled from `@prezly/sdk` to get rid of direct dependency requirement
export enum TrackingPolicy {
/**
Expand Down

0 comments on commit 3c6763a

Please sign in to comment.