@taxi_tabby/react-eventflow
v0.3.1
Published
React event tracking library with Provider pattern for collecting user interactions and sending to backend
Downloads
909
Maintainers
Readme
@taxi_tabby/react-eventflow
A React-based event tracking library that collects user interactions using the Provider pattern and sends them to your backend.
2025년에도 여전히 웹의 황제 자리를 꽉 잡고 있는 React.
여기 소개할 방법은 React에서 구현할 수 있는 가장 심플하고 매력적인 사용자 추적 방식입니다.
사실 통계나 추적이라는 게 기술적으로 100% 정확할 수는 없어요. 우회 방법이 넘쳐나거든요.
뭐, 인터넷 실명제를 할 것도 아니고 당신의 집에 글로벌 라우터가 있는 것도 아니시겠죠
그냥 빨리 포기하고 현실적인 방법을 찾는 게 속 편합니다.
제가 개발하면서 느낀 “진짜 필요한 정보”는 딱 이 정도였습니다:
- 어디서 들어왔는지
- 지금 어디를 보고 있는지
- 어디로 이동하려 하는지
- 클릭 여부 + 클릭 위치
- 스크롤 여부 + 스크롤 위치
- 마우스가 어디서 어디로 이동했는지
- 좌클릭/우클릭/휠 클릭 여부
- 어딜 보고 있는가 (화면 스크린샷 정보 개발 예정)
- 그리고 가장 중요한 것: 사용자가 누군지는 몰라도, 모든 행동이 한 사용자에게 연결되고 있는가
말했듯 완벽하게 정확할 순 없지만, 사용자 행동의 흐름을 이어주는 것까지는 충분히 가능합니다.
그래서 이벤트 발생 시간과 함께 ‘나름~ 고유한 지문 데이터’를 제공하는 방식으로 정리했습니다.
물론 더 깊게 추적할 수도 있겠지만, 굳이 무거울 필요는 없죠. ip 찾고 dns 조회하고 트래킹하고 국가 위치니 어쩌니 찾고 어휴 무거워..
단순하게 여기서 수집한 핵심 데이터만 백엔드로 툭 던져줍시다
거기서 알아서 잘 처리하겠죠 뭐. 우리는 필요한 건 이미 넘겼으니까요.
로그인 사용자나 더욱 개인을 특정하고자 한다면 onEvent 에서 값 얻어 불러와서 쏴주면 되고 쉽다 쉬워
굳이 무겁게 하려면 백엔드 서버에서 알아서 하시길 권장!
* 주의 *
trackReferral 기능은 불안정합니다! 잘 동작한다고 생각한다면 따봉
Features
- ✅ Built on React Provider pattern
- ✅ Full TypeScript support
- ✅ Automatic pageview tracking
- ✅ Automatic navigation tracking
- ✅ Automatic referral/traffic source tracking
- ✅ UTM parameter tracking for marketing campaigns
- ✅ Mouse movement and click tracking
- ✅ Scroll tracking
- ✅ Event batching support
- ✅ Browser fingerprinting for user identification
- ✅ HMAC signature verification for server-side validation
- ✅ Lightweight with minimal dependencies
Installation
npm install @taxi_tabby/react-eventflowor
yarn add @taxi_tabby/react-eventflowBasic Usage
1. Setup Provider
Add EventFlowProvider at the root of your app:
import { EventFlowProvider } from '@taxi_tabby/react-eventflow';
function App() {
return (
<EventFlowProvider
config={{
onEvent: async (event) => {
// Send event to your backend
await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
},
trackPageViews: true,
trackNavigation: true,
trackReferral: true,
trackMouseClick: true, // Enable click tracking
trackMouseMoving: false, // Enable mouse movement tracking (can be noisy)
trackScroll: true, // Enable scroll tracking
debug: process.env.NODE_ENV === 'development',
}}
>
<YourApp />
</EventFlowProvider>
);
}2. Manual Page View Tracking
Use the useEventFlow hook in your components:
import { useEventFlow } from '@taxi_tabby/react-eventflow';
function CustomPage() {
const { trackPageView } = useEventFlow();
useEffect(() => {
trackPageView('/custom-page', 'Custom Page Title');
}, []);
return <div>...</div>;
}Configuration Options
interface EventFlowConfig {
/** Event callback function (required) */
onEvent: (event: EventData | EventData[]) => void | Promise<void>;
/** Enable automatic pageview tracking (default: true) */
trackPageViews?: boolean;
/** Enable automatic navigation tracking (default: true) */
trackNavigation?: boolean;
/** Enable automatic referral tracking (default: true) */
trackReferral?: boolean;
/** Enable automatic mouse click tracking (default: false) */
trackMouseClick?: boolean;
/** Enable automatic mouse moving tracking (default: false) */
trackMouseMoving?: boolean;
/** Mouse moving throttle interval in ms (default: 100) */
mouseMovingThrottle?: number;
/** Enable automatic scroll tracking (default: false) */
trackScroll?: boolean;
/** Scroll throttle interval in ms (default: 200) */
scrollThrottle?: number;
/** Debug mode (default: false) */
debug?: boolean;
/** Enable event batching (default: false) */
enableBatching?: boolean;
/** Batching interval in ms (default: 2000) */
batchInterval?: number;
/** Enable HMAC signature (default: false) */
enableHmac?: boolean;
/** HMAC secret key (required when enableHmac is true) */
hmacSecretKey?: string;
/** HMAC hash algorithm (default: 'sha256') */
hmacAlgorithm?: 'sha256' | 'sha512' | 'sha384' | 'sha1';
/** HMAC output encoding (default: 'hex') */
hmacEncoding?: 'hex' | 'base64' | 'base64url';
}Event Types
All events include a fingerprint field for unique user identification.
PageViewEvent
{
type: 'pageview',
timestamp: 1699999999999,
fingerprint: 'unique-browser-id',
payload: {
url: '/products',
title: 'Products Page',
referrer: 'https://google.com',
userAgent: 'Mozilla/5.0...'
}
}ReferralEvent
{
type: 'referral',
timestamp: 1699999999999,
fingerprint: 'unique-browser-id',
payload: {
currentUrl: 'https://example.com/products?utm_source=google',
referrer: 'https://google.com/search?q=products',
referrerDomain: 'google.com',
sourceType: 'search', // 'direct' | 'external' | 'internal' | 'social' | 'search' | 'email' | 'unknown'
utm: {
source: 'google',
medium: 'cpc',
campaign: 'summer_sale',
term: 'products',
content: 'ad_variant_a'
},
queryParams: {
utm_source: 'google',
utm_medium: 'cpc',
// ... all query parameters
},
navigation: {
historyLength: 5,
isBackNavigation: false,
navigationType: 'navigate'
}
}
}NavigationEvent
{
type: 'navigation',
timestamp: 1699999999999,
fingerprint: 'unique-browser-id',
payload: {
from: '/home',
to: '/products'
}
}MouseMovingEvent
{
type: 'mouse-moving',
timestamp: 1699999999999,
fingerprint: 'unique-browser-id',
payload: {
x: 100,
y: 200,
pageX: 100,
pageY: 500
}
}MouseClickEvent
{
type: 'mouse-click',
timestamp: 1699999999999,
fingerprint: 'unique-browser-id',
payload: {
x: 100,
y: 200,
target: 'button',
targetClass: 'btn-primary',
targetId: 'submit-btn',
button: 0
}
}ScrollEvent
{
type: 'scroll',
timestamp: 1699999999999,
fingerprint: 'unique-browser-id',
payload: {
scrollY: 500,
scrollX: 0,
scrollDepth: 25,
documentHeight: 2000
}
}Advanced Features
Event Batching
Enable batching to reduce network requests and payload size:
<EventFlowProvider
config={{
onEvent: handleEvents,
enableBatching: true,
batchInterval: 5000, // Send every 5 seconds
}}
>
<App />
</EventFlowProvider>When batching is enabled, events are sent in this optimized format:
// Batched events (fingerprint sent once for all events)
{
fingerprint: 'abc123def456',
events: [
{
type: 'pageview',
timestamp: 1699999999999,
payload: { url: '/home', title: 'Home' }
},
{
type: 'mouse-click',
timestamp: 1699999999999,
payload: { x: 100, y: 200, target: 'button' }
},
{
type: 'scroll',
timestamp: 1700000000000,
payload: { scrollY: 500, scrollDepth: 25 }
}
]
}This format significantly reduces payload size by including the fingerprint only once instead of with each event.
Interaction Tracking
Enable different types of user interaction tracking:
<EventFlowProvider
config={{
onEvent: handleEvents,
// Basic tracking (enabled by default)
trackPageViews: true,
trackNavigation: true,
trackReferral: true,
// Interaction tracking (disabled by default)
trackMouseClick: true, // Track all click events
trackMouseMoving: true, // Track mouse movements (can generate many events)
mouseMovingThrottle: 100, // Send mouse move event every 100ms
trackScroll: true, // Track scroll depth
scrollThrottle: 200, // Send scroll event every 200ms
}}
>
<App />
</EventFlowProvider>Note on Performance:
- Mouse moving tracking can generate a lot of events. Use
mouseMovingThrottleto control frequency. - Scroll tracking only sends events when scroll depth increases.
- All trackers use
passiveevent listeners to avoid blocking the main thread. - Error handling prevents conflicts with other libraries.
User Identification
Every event automatically includes a browser fingerprint for user identification:
{
fingerprint: 'abc123def456', // Unique per browser
type: 'pageview',
// ...
}This fingerprint is:
- Generated using browser characteristics
- Persistent across sessions
- Privacy-friendly (no personal data)
HMAC Signature Verification
Enable HMAC signatures to verify events on your server and prevent tampering:
<EventFlowProvider
config={{
onEvent: handleEvents,
enableHmac: true,
hmacSecretKey: process.env.REACT_APP_HMAC_SECRET, // Keep this secret!
hmacAlgorithm: 'sha256', // Default: 'sha256' (options: 'sha256', 'sha512', 'sha384', 'sha1')
hmacEncoding: 'hex', // Default: 'hex' (options: 'hex', 'base64', 'base64url')
}}
>
<App />
</EventFlowProvider>⚠️ Security Warning: Client-side environment variables (
REACT_APP_*,NEXT_PUBLIC_*) are embedded in your bundle at build time and can be viewed by anyone!Recommended Approach: Implement a proper key management infrastructure on your backend for maximum security and effectiveness.
Instead of embedding HMAC keys directly in your client bundle, your backend should:
- Generate and distribute session-specific temporary keys to authenticated clients
- Derive temporary keys from a master secret that never leaves the server
- Implement automatic key rotation and expiration (e.g., 1-hour TTL)
- Store session keys in a secure backend store (Redis, database, or in-memory with proper session management)
This approach follows Public Key Infrastructure (PKI) principles, where:
- The master key remains on the server and is never exposed
- Each client session receives a unique derived key
- Compromised keys have limited scope and impact
- Keys can be revoked and rotated without affecting all users
Benefits:
- ✅ Master key stays on server only (never exposed)
- ✅ Different key per session (minimizes impact if key is compromised)
- ✅ Automatic key expiration and rotation
- ✅ Can integrate with user authentication
- ✅ Centralized key management and audit capabilities
- ✅ Compliance with security best practices
When HMAC is enabled, events include a signature:
// Single event
{
type: 'pageview',
timestamp: 1699999999999,
fingerprint: 'abc123def456',
hmac: 'a1b2c3d4e5f6...', // HMAC signature of entire event data
payload: { url: '/home' }
}
// Batched events
{
fingerprint: 'abc123def456',
hmac: 'a1b2c3d4e5f6...', // HMAC signature of entire batch data
events: [...]
}The HMAC signature covers the entire event data (type, timestamp, fingerprint, and payload) to ensure complete data integrity.
Server-side verification example (Node.js):
import crypto from 'crypto';
function verifyEventHmac(event: EventData, secretKey: string): boolean {
const { hmac, ...eventData } = event;
// Create normalized JSON string from event data (excluding hmac)
const normalizedData = JSON.stringify(eventData);
const expectedHmac = crypto
.createHmac('sha256', secretKey)
.update(normalizedData)
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(expectedHmac)
);
}
// For batched events
function verifyBatchHmac(batch: BatchedEvents, secretKey: string): boolean {
const { hmac, ...batchData } = batch;
// Create normalized JSON string from batch data (excluding hmac)
const normalizedData = JSON.stringify(batchData);
const expectedHmac = crypto
.createHmac('sha256', secretKey)
.update(normalizedData)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(hmac),
Buffer.from(expectedHmac)
);
}
// In your API handler
app.post('/api/events', (req, res) => {
const event = req.body;
if (!verifyEventHmac(event, process.env.HMAC_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process valid event
// ...
});Why use HMAC?
- Prevents event tampering - ensures complete data integrity of type, timestamp, fingerprint, and payload
- Validates that events come from your application
- Detects any modifications to event data during transmission
- Simple server-side verification
- Cryptographically secure using industry-standard algorithms
Best Practices:
- Use
sha256orsha512algorithm for best security - Keep your
hmacSecretKeysecret and never expose it in client code - Use environment variables to store the secret key
- The same secret key must be used on both client and server
- Use
hexencoding for compatibility,base64urlfor shorter signatures
Eaxample
next.js
'use client';
import { EventFlowProvider as BaseEventFlowProvider } from '@taxi_tabby/react-eventflow';
import { ReactNode } from 'react';
interface EventFlowWrapperProps {
children: ReactNode;
}
export function EventFlowProvider({ children }: EventFlowWrapperProps) {
return (
<BaseEventFlowProvider
config={{
trackPageViews: true,
trackNavigation: true,
trackReferral: true,
trackMouseClick: true,
trackMouseMoving: true,
trackScroll: true,
enableBatching: true,
batchInterval: 5000,
mouseMovingThrottle: 1000,
scrollThrottle: 1000,
// HMAC signature for server verification
enableHmac: true,
hmacSecretKey: process.env.NEXT_PUBLIC_HMAC_SECRET!,
hmacAlgorithm: 'sha256',
hmacEncoding: 'hex',
debug: false,
onEvent: async (event) => {
// Check if it's a batched event
if ('events' in event) {
// Batched events: { fingerprint: string, hmac: string, events: Array }
console.log('Batch received:', event.fingerprint, event.events.length, 'events');
// Send batched events to your backend
await fetch('/api/events/batch', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
} else {
// Single event: { fingerprint: string, hmac: string, type: string, timestamp: number, payload: any }
console.log('Single event:', event.fingerprint, event.type);
await fetch('/api/events', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event),
});
}
},
}}
>
{children}
</BaseEventFlowProvider>
);
}License
MIT
Contributing
Issues and PRs are always welcome!
