A minimal fullstack user activity tracker inspired by Google Analytics, built with Node.js, TypeScript, and MongoDB.
This service tracks user events on static HTML pages, buffers them in the browser, and sends them to a backend for storage and analysis.
📦 Vite (port 50000) — Frontend Dev Server
- Serves: 1.html, 2.html, 3.html
- Used as a static HTML server during development
- Also compiles tracker.ts into a UMD bundle (dist/tracker.umd.js)
⸻
🚀 Fastify (port 8888) — Backend Server
- Serves: GET /tracker → returns the compiled tracker.umd.js from Vite’s dist/ directory
- Receives: POST /track → accepts analytics events sent from the client
- Stores: Analytics events in MongoDB using bulk insert
⚙️ May also provide CORS headers, compression, and other production optimizations
yarn client:dev- Open http://localhost:50000/
- ✅ Hot reload via Vite
- ❌ No backend interaction by default
yarn db:up # For database by Docker (optional)
yarn server:dev- Access backend endpoint: http://localhost:8888/track
- ✅ Hot reload via tsx
- ✅ Good for developing Fastify API
yarn db:up # For database by Docker (optional)
yarn client:build # Build both client and tracker
yarn server:start # Start backend to serve tracker and handle events
yarn client:dev # Serve static HTML pages- Open a test page like: http://localhost:50000/1.html
- The page loads tracker from:
http://localhost:8888/tracker - Events are sent to:
http://localhost:8888/track - ✅ Analytics pipeline fully tested end-to-end
Receives an array of events and stores them in MongoDB. Events are validated before storing.
[
{
"event": "click-button",
"tags": ["some", "optional", "tags"],
"url": "http://localhost:50000/1.html",
"title": "My Website",
"ts": 1675209600
}
]- ✅ Valid input →
200 OK - ❌ Invalid input →
422 Unprocessable Entity
Returns the minified JS tracker script that will be injected in each page.
- ✅ Custom snippet for tracker injection (like Google Analytics)
- ✅ Ensures events are sent before link navigation
- ✅ Avoids preflight CORS requests by tweaking headers
- Backend: Node.js + Fastify
- DB: MongoDB
- Language: TypeScript (both client & server)
- Formatter: Prettier, Linter
- Init project structure (
client/,server/,pages/) - Add TypeScript, tsconfig, Lint, Prettier
- Configure precommit by Husky
- Setup MongoDB connection (docker-compose)
- Create
1.html,2.html,3.htmlin/pages - Serve pages on port 50000
- Include tracker via
<script src="...">
- Create global
trackerobject - Implement
.track(event, ...tags) - Append url, title, ts to each event
- Buffer events
- Flush if 3+ events
- Flush if 1s passed
- Flush on unload
- Retry on failure
- Setup Fastify server on port 8888
- Add
/trackerroute to serve JS file - Add
/trackroute to accept event array - Add
/eventroute to get event array - Add CORS
- Validate input format via Zod
- Respond 200 OK or 422 Unprocessable
- Insert events to MongoDB (bulk insert)
- Respond without waiting for DB write
- Create snippet for async script injection
- Prevent navigation until flush completes (Beacon API)
- Avoid preflight request (CORS optimizations)
- Write
README.mdwith instructions - Add log output for debugging
- Test in Chrome
- Add summary of time spent and challenges in PR description
Total: ~4-6 hours
Решил комбинировать 2 вида запросов: fetch & beacon
- fetch в обычно ситуации чтобы знать о проблемах с сетью и иметь возможность выполнить ретраи
- beacon в случае ухода со страницы для гарантированной отправки
Основная сложность: Сложнее всего было точно выяснить требования в условиях нарушенной коммуникации с заказчиком. Это требовало самостоятельно принимать решения и выявлять логические несостыковки в условиях. Я рассматривал это как часть задания и старался поднимать подобные вопросы заранее.
Касательно сниппета: главное преимущество в том, что мы
- не блокируем загрузку основной страницы
- при этом уже собираем статистику до загрузки скрипта
Мои замечания по условиям:
- Если во время ожидания секунды после сбоя сети в буфер придет 3 новых события, они будут немедленно отправлены на сервер? Если да:
- Мы нарушаем порядок отправки событий. Новые события уходят раньше тех, которые уже были в буфере.
- Мы предполагаем, что проблема в сети, а не в самих событиях. Зачем повторно делать запрос раньше таймаута, если мы только что убедились, что соединение не работает?
- В результате мы делаем лишние запросы при заведомо плохой сети, что противоречит цели — минимизировать их количество.
Было бы логичнее: после неудачной попытки замораживать отправку на 1 секунду полностью (независимо от количества событий в буфере), и только после таймаута пробовать снова. Исключение — если пользователь уходит со страницы.
- Если мы получаем на сервер 10 событий, 9 из которых валидно, а 1 нет - то мы отвечаем 422 и не принимаем ни одно событие то есть мы просто теряем 9 валидных сбытий. Возможно, было бы логичнее обрабатывать валидные события и возвращать мультистатус 207