0
Fork 0
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:
Gao Sun 2023-07-29 17:44:03 +08:00 committed by GitHub
parent 28da86bfe4
commit a8a26a5299
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 108 additions and 4 deletions

View file

@ -1,3 +1,6 @@
import { webcrypto } from 'node:crypto';
import { TextEncoder, TextDecoder } from 'node:util';
import i18next from 'i18next'; import i18next from 'i18next';
import { initReactI18next } from 'react-i18next'; import { initReactI18next } from 'react-i18next';
@ -7,3 +10,11 @@ void i18next.use(initReactI18next).init({
lng: 'en', lng: 'en',
react: { useSuspense: false }, 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 */

View file

@ -1,6 +1,8 @@
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import useCurrentUser from '@/hooks/use-current-user';
import { import {
shouldReport, shouldReport,
lintrk, lintrk,
@ -8,6 +10,9 @@ import {
linkedInConversionId, linkedInConversionId,
gtag, gtag,
gtagSignUpConversionId, gtagSignUpConversionId,
rdt,
redditPixelId,
hashEmail,
} from './utils'; } from './utils';
/** /**
@ -15,6 +20,28 @@ import {
* Insight Tag, then reports a sign-up conversion to them. * Insight Tag, then reports a sign-up conversion to them.
*/ */
export default function ReportConversion() { 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. * 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, * 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}`} 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://snap.licdn.com/li.lms-analytics/insight.min.js" />
<script async src="https://www.redditstatic.com/ads/pixel.js" />
</Helmet> </Helmet>
); );
} }

View file

@ -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);
});
});

View file

@ -12,7 +12,7 @@ export const linkedInConversionId = '13374828';
*/ */
export const shouldReport = window.location.hostname.endsWith('.' + logtoProductionHostname); 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. */ /** This function is edited from the Google Tag official code snippet. */
export function gtag(..._: unknown[]) { 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 // 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); window.dataLayer.push(arguments);
} }
@ -37,8 +37,45 @@ export function lintrk(..._: unknown[]) {
window.lintrk = { q: [] }; window.lintrk = { q: [] };
} }
// eslint-disable-next-line prefer-rest-params
window.lintrk.q.push(arguments); 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 */

View file

@ -6,4 +6,8 @@ declare interface Window {
lintrk?: { lintrk?: {
q: unknown[]; q: unknown[];
}; };
// Reddit
rdt?: {
callQueue: unknown[];
};
} }