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 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 */
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
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 */
|
||||||
|
|
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?: {
|
lintrk?: {
|
||||||
q: unknown[];
|
q: unknown[];
|
||||||
};
|
};
|
||||||
|
// Reddit
|
||||||
|
rdt?: {
|
||||||
|
callQueue: unknown[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue