mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(console): report reddit conversion (#4263)
* refactor(console): report reddit conversion * refactor(console): update pixel id
This commit is contained in:
parent
28da86bfe4
commit
a8a26a5299
5 changed files with 108 additions and 4 deletions
|
@ -1,3 +1,6 @@
|
|||
import { webcrypto } from 'node:crypto';
|
||||
import { TextEncoder, TextDecoder } from 'node:util';
|
||||
|
||||
import i18next from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
|
||||
|
@ -7,3 +10,11 @@ void i18next.use(initReactI18next).init({
|
|||
lng: 'en',
|
||||
react: { useSuspense: false },
|
||||
});
|
||||
|
||||
/* eslint-disable @silverhand/fp/no-mutation */
|
||||
// @ts-expect-error monkey-patch for `crypto`
|
||||
crypto.subtle = webcrypto.subtle;
|
||||
global.TextEncoder = TextEncoder;
|
||||
// @ts-expect-error monkey-patch for `TextEncoder`/`TextDecoder`
|
||||
global.TextDecoder = TextDecoder;
|
||||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { useEffect } from 'react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
|
||||
import useCurrentUser from '@/hooks/use-current-user';
|
||||
|
||||
import {
|
||||
shouldReport,
|
||||
lintrk,
|
||||
|
@ -8,6 +10,9 @@ import {
|
|||
linkedInConversionId,
|
||||
gtag,
|
||||
gtagSignUpConversionId,
|
||||
rdt,
|
||||
redditPixelId,
|
||||
hashEmail,
|
||||
} from './utils';
|
||||
|
||||
/**
|
||||
|
@ -15,6 +20,28 @@ import {
|
|||
* Insight Tag, then reports a sign-up conversion to them.
|
||||
*/
|
||||
export default function ReportConversion() {
|
||||
const { user, isLoading } = useCurrentUser();
|
||||
|
||||
/**
|
||||
* Initiate Reddit Pixel and report a sign-up conversion to it when user is loaded.
|
||||
* Use user email to prevent duplicate conversion, and it is hashed before sending
|
||||
* to protect user privacy.
|
||||
*/
|
||||
useEffect(() => {
|
||||
const report = async () => {
|
||||
rdt('init', redditPixelId, {
|
||||
optOut: false,
|
||||
useDecimalCurrencyValues: true,
|
||||
email: await hashEmail(user?.primaryEmail ?? undefined),
|
||||
});
|
||||
rdt('track', 'SignUp');
|
||||
};
|
||||
|
||||
if (shouldReport && !isLoading) {
|
||||
void report();
|
||||
}
|
||||
}, [user, isLoading]);
|
||||
|
||||
/**
|
||||
* This `useEffect()` initiates Google Tag and report a sign-up conversion to it.
|
||||
* It may run multiple times (e.g. a user visit multiple times to finish the onboarding process,
|
||||
|
@ -49,6 +76,7 @@ export default function ReportConversion() {
|
|||
src={`https://www.googletagmanager.com/gtag/js?id=${gtagAwTrackingId}`}
|
||||
/>
|
||||
<script async src="https://snap.licdn.com/li.lms-analytics/insight.min.js" />
|
||||
<script async src="https://www.redditstatic.com/ads/pixel.js" />
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { hashEmail } from './utils';
|
||||
|
||||
describe('hashEmail()', () => {
|
||||
it('should return undefined if the given email is falsy', async () => {
|
||||
expect(await hashEmail()).toBeUndefined();
|
||||
expect(await hashEmail('')).toBeUndefined();
|
||||
expect(await hashEmail(' ')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined if the given email is invalid', async () => {
|
||||
expect(await hashEmail('foo')).toBeUndefined();
|
||||
expect(await hashEmail('foo@')).toBeUndefined();
|
||||
expect(await hashEmail('@foo')).toBeUndefined();
|
||||
expect(await hashEmail('foo@bar@baz')).toBeUndefined();
|
||||
expect(await hashEmail('foo@ bar .@')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the hash of the canonicalized email', async () => {
|
||||
const hash = 'ff8d9819fc0e12bf0d24892e45987e249a28dce836a85cad60e28eaaa8c6d976';
|
||||
expect(await hashEmail('alice@example.com')).toBe(hash);
|
||||
expect(await hashEmail('Al.ice+Apple@Example.Com')).toBe(hash);
|
||||
expect(await hashEmail(' a.Lice+@example.com')).toBe(hash);
|
||||
});
|
||||
});
|
|
@ -12,7 +12,7 @@ export const linkedInConversionId = '13374828';
|
|||
*/
|
||||
export const shouldReport = window.location.hostname.endsWith('.' + logtoProductionHostname);
|
||||
|
||||
/* eslint-disable @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods */
|
||||
/* eslint-disable @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods, prefer-rest-params */
|
||||
|
||||
/** This function is edited from the Google Tag official code snippet. */
|
||||
export function gtag(..._: unknown[]) {
|
||||
|
@ -21,7 +21,7 @@ export function gtag(..._: unknown[]) {
|
|||
}
|
||||
|
||||
// We cannot use rest params here since gtag has some internal logic about `arguments` for data transpiling
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
|
||||
window.dataLayer.push(arguments);
|
||||
}
|
||||
|
||||
|
@ -37,8 +37,45 @@ export function lintrk(..._: unknown[]) {
|
|||
window.lintrk = { q: [] };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
window.lintrk.q.push(arguments);
|
||||
}
|
||||
|
||||
/* eslint-enable @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods */
|
||||
/**
|
||||
* This function will do the following things:
|
||||
*
|
||||
* 1. Canonicalize the given email by Reddit's rule: lowercase, trim,
|
||||
* remove dots, remove everything after the first '+'.
|
||||
* 2. Hash the canonicalized email by SHA256.
|
||||
*/
|
||||
export const hashEmail = async (email?: string) => {
|
||||
if (!email) {
|
||||
return;
|
||||
}
|
||||
|
||||
const splitEmail = email.toLocaleLowerCase().trim().split('@');
|
||||
const [localPart, domain] = splitEmail;
|
||||
|
||||
if (!localPart || !domain || splitEmail.length > 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/prefer-string-replace-all
|
||||
const canonicalizedEmail = `${localPart.replace(/\./g, '').replace(/\+.*/, '')}@${domain}`;
|
||||
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(canonicalizedEmail));
|
||||
|
||||
// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex
|
||||
return [...new Uint8Array(hash)].map((value) => value.toString(16).padStart(2, '0')).join('');
|
||||
};
|
||||
|
||||
export const redditPixelId = 't2_ggt11omdo';
|
||||
|
||||
/** Report Reddit conversion events. */
|
||||
export function rdt(..._: unknown[]) {
|
||||
if (!window.rdt) {
|
||||
window.rdt = { callQueue: [] };
|
||||
}
|
||||
|
||||
window.rdt.callQueue.push(arguments);
|
||||
}
|
||||
|
||||
/* eslint-enable @silverhand/fp/no-mutation, @silverhand/fp/no-mutating-methods, prefer-rest-params */
|
||||
|
|
4
packages/console/src/include.d/tags.d.ts
vendored
4
packages/console/src/include.d/tags.d.ts
vendored
|
@ -6,4 +6,8 @@ declare interface Window {
|
|||
lintrk?: {
|
||||
q: unknown[];
|
||||
};
|
||||
// Reddit
|
||||
rdt?: {
|
||||
callQueue: unknown[];
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue