Skip to content

Commit 21255a9

Browse files
authored
feat(explorer): enhance anti-bot protection (#126)
# Description of change enhance anti-bot protection by initializing Amplitude on user interaction.
1 parent 22ee9ad commit 21255a9

1 file changed

Lines changed: 45 additions & 66 deletions

File tree

apps/explorer/src/lib/utils/analytics/amplitude.ts

Lines changed: 45 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,54 @@ const IS_ENABLED =
1414

1515
const IS_DEV = import.meta.env.VITE_BUILD_ENV !== 'production';
1616

17+
// Guards against duplicate listener registration on repeated initAmplitude calls.
18+
let humanWaitSetup = false;
19+
// Buffered network value set before Amplitude loads; replayed on first human interaction.
20+
let pendingNetwork: string | null = null;
21+
1722
/**
18-
* Anti-bot configuration: Events are queued but not sent until a human interaction is detected.
19-
* Sessions are classified as human on the first DOM interaction (scroll, mousemove, keydown, touchstart).
23+
* Anti-bot protection: defers ampli.load() until a genuine human gesture is detected.
2024
*/
21-
const ANTI_BOT_CONFIG = {
22-
// Regular flush interval once the session is classified as human
23-
REGULAR_FLUSH_INTERVAL_MS: 1000,
24-
// Initial flush settings — effectively disabled so events queue locally until bot check passes
25-
INITIAL_FLUSH_INTERVAL_MS: 3600000, // 1 hour
26-
INITIAL_QUEUE_SIZE: 500,
27-
} as const;
25+
export function initAmplitude(): void {
26+
const consentStatus = getAmplitudeConsentStatus();
27+
28+
if (ampli.isLoaded || humanWaitSetup || consentStatus === 'declined') {
29+
return;
30+
}
2831

29-
let isBotCleared = false;
32+
if (navigator.webdriver) {
33+
return;
34+
}
3035

31-
export async function initAmplitude() {
32-
const consentStatus = getAmplitudeConsentStatus();
36+
humanWaitSetup = true;
37+
waitForHumanInteraction();
38+
}
39+
40+
const HUMAN_SIGNAL_EVENTS = ['pointerdown', 'wheel', 'keydown', 'touchstart', 'copy'] as const;
41+
42+
function waitForHumanInteraction(): void {
43+
const controller = new AbortController();
44+
let handled = false;
45+
46+
function onHumanInteraction() {
47+
if (handled) return;
48+
handled = true;
49+
controller.abort();
50+
void loadAmplitude();
51+
}
3352

53+
const options = { passive: true, signal: controller.signal } as const;
54+
for (const event of HUMAN_SIGNAL_EVENTS) {
55+
window.addEventListener(event, onHumanInteraction, options);
56+
}
57+
}
58+
59+
async function loadAmplitude(): Promise<void> {
60+
const consentStatus = getAmplitudeConsentStatus();
3461
if (ampli.isLoaded || consentStatus === 'declined') {
3562
return;
3663
}
3764

38-
// Load Amplitude with anti-bot flush settings
3965
await ampli.load({
4066
environment: 'iotaexplorer',
4167
disabled: !IS_ENABLED,
@@ -55,77 +81,30 @@ export async function initAmplitude() {
5581
pageUrlEnrichment: IS_ENABLED,
5682
},
5783
logLevel: LogLevel.None,
58-
flushIntervalMillis: ANTI_BOT_CONFIG.INITIAL_FLUSH_INTERVAL_MS,
59-
flushQueueSize: ANTI_BOT_CONFIG.INITIAL_QUEUE_SIZE,
6084
identityStorage: 'localStorage',
6185
},
6286
},
6387
}).promise;
6488

6589
ampli.client.add(attachEnvironmentPlugin(IS_DEV));
6690

67-
setupAntiBotProtection();
68-
}
69-
70-
const HUMAN_SIGNAL_EVENTS = ['scroll', 'mousemove', 'keydown', 'touchstart'] as const;
71-
72-
/**
73-
* Sets up anti-bot protection:
74-
* 1. Queues all events locally (1-hour flush interval prevents premature sends)
75-
* 2. Classifies the session as human on the first DOM interaction and enables regular flushing
76-
* 3. On page exit, beacon-flushes only if the session was classified as human
77-
*/
78-
function setupAntiBotProtection() {
79-
let flushInterval: ReturnType<typeof setInterval> | null = null;
80-
81-
function enableFlushing() {
82-
if (isBotCleared) {
83-
return;
84-
}
85-
isBotCleared = true;
86-
ampli.flush();
87-
flushInterval = setInterval(() => {
88-
if (ampli.isLoaded) {
89-
ampli.flush();
90-
}
91-
}, ANTI_BOT_CONFIG.REGULAR_FLUSH_INTERVAL_MS);
91+
if (pendingNetwork !== null) {
92+
setAmplitudeIdentity(pendingNetwork);
9293
}
9394

94-
const humanSignalController = new AbortController();
95-
const options = { passive: true, signal: humanSignalController.signal } as const;
96-
const handler = () => {
97-
humanSignalController.abort();
98-
enableFlushing();
99-
};
100-
for (const event of HUMAN_SIGNAL_EVENTS) {
101-
window.addEventListener(event, handler, options);
102-
}
103-
104-
// Flush on page exit only if the session was classified as human.
10595
window.addEventListener(
10696
'pagehide',
10797
() => {
108-
humanSignalController.abort();
109-
110-
if (flushInterval) {
111-
clearInterval(flushInterval);
112-
}
113-
114-
if (isBotCleared) {
115-
ampli.client.setTransport('beacon');
116-
ampli.flush();
117-
}
98+
ampli.client.setTransport('beacon');
99+
ampli.flush();
118100
},
119101
{ once: true },
120102
);
121103
}
122104

123-
/**
124-
* Set the Amplitude user identity with the current network context.
125-
* Updates user property: network.
126-
* This allows filtering and segmenting analytics events by network dimension.
127-
*/
128105
export function setAmplitudeIdentity(network: string): void {
106+
pendingNetwork = network;
107+
129108
if (!ampli.isLoaded) {
130109
return;
131110
}

0 commit comments

Comments
 (0)