stone-analytics
v0.0.136
Published
analytic package STONE
Keywords
Readme
Stone Analytics
NPM-пакет клиентской аналитики для сайтов STONE. Собирает поведенческие события, обогащает их контекстом страницы и идентификаторами пользователя, отправляет в Google Tag Manager (window.dataLayer) и на бэкенд stone.ru/api/analytics/send-event.
Оглавление
- Stone Analytics
Зачем нужен
На сайтах STONE (stone.ru, realty, лендинги проектов) нужна единая клиентская аналитика: одни и те же идентификаторы, контекст страницы и формат событий во всех фронтенд-приложениях. Пакет решает четыре ключевые задачи:
- Идентификация пользователя. Генерирует и поддерживает
st_client_id(на год) иst_session_id(на сессию) вlocalStorage. Сессия сбрасывается при закрытии последней вкладки или после 30 минут неактивности. - Обогащение событий контекстом. Автоматически определяет
page_type,page_direction,page_project,page_lotпо URL. Подключает идентификаторы Яндекс.Метрики, Comagic, FingerprintJS, GA, данные A/B-теста и авторизации. - Единый dataLayer для GTM. Все события (pageview, click, submit и др.) пушатся в
window.dataLayerв стандартизированном формате — GTM-триггеры обрабатывают их без дублирования логики на каждом сайте. - Прямая отправка на бэкенд. Модуль
sendAnalyticsToAPIотправляет события наhttps://stone.ru/api/analytics/send-event(используется внутри пакета и доступен для прямого вызова).
Стек технологий
| Технология | Назначение |
| --- | --- |
| TypeScript 5 | Типизация исходного кода |
| tsup | Сборка CJS/ESM + генерация .d.ts |
| MobX 6 | Реактивный стор StoneDataLayerStore |
| mobx-react-lite | Интеграция MobX с React |
| @fingerprintjs/fingerprintjs | Браузерный fingerprint (st_client_hash_id) |
| cookies-next | Чтение cookie GA (_ga) |
| js-md5 | Хеширование виртуального номера телефона |
| React 18+ | Peer-зависимость: хуки useInitializeMetrics, useComagicStatus |
Архитектура
Принципиальная схема пакета
flowchart TB
subgraph consumer [Next.js / React приложение]
AppProvider["AppProvider + useInitializeMetrics"]
Components["Компоненты сайта"]
end
subgraph package [stone-analytics]
SessionId["sessionClientId"]
Store["StoneDataLayerStore"]
PageUtils["pageType / pageDirection / getProjectName / getLotName"]
Events["eventPageView / eventClick / eventSubmit / ..."]
Utils["utils: fingerprint, GA, YM, AB cookie"]
SendAPI["sendAnalyticsToAPI"]
DataLayerPush["dataLayerPush"]
end
subgraph browser [Браузер]
LS[("localStorage")]
DL["window.dataLayer"]
GTM["Google Tag Manager"]
YM["Яндекс.Метрика"]
Comagic["Comagic"]
end
subgraph backend [Бэкенд STONE]
API["POST /api/analytics/send-event"]
end
AppProvider --> SessionId
SessionId --> LS
Components --> Store
Store --> PageUtils
Store --> Events
Events --> Utils
Events -->|"pushLayer"| DL
DL --> GTM
Utils --> YM
Utils --> Comagic
SendAPI --> API
DataLayerPush --> DLМодули
Пакет состоит из шести логических модулей:
- Hooks (src/hooks/useInitializeMetrics.ts) — React-хук инициализации
st_client_id/st_session_id, управление жизненным циклом сессии (счётчик вкладок, таймер неактивности 30 мин). - Session / Client ID (src/utils/sessionClientId.ts) — генерация time-based UID, проверка годового срока
st_client_id. - StoneDataLayerStore (src/store/StoneDataLayerStore/StoneDataLayerStore.ts) — MobX-стор: хранит контекст страницы, предоставляет методы
setEvent*для каждого типа события, пушит вwindow.dataLayer. - Events (src/store/StoneDataLayerStore/events/) — обработчики конкретных событий: формируют payload из
getDefaultAnalyticsData+ специфичные поля. - Page context (src/store/StoneDataLayerStore/utils/) — определение типа страницы, направления, проекта и лота по pathname/href через regex-правила (regs.ts).
- Transport — sendAnalyticsToAPI (HTTP на бэкенд), dataLayerPush (GTM dataLayer), ymTarget (reachGoal в Яндекс.Метрику).
Диаграмма взаимодействия
flowchart LR
subgraph init [Инициализация]
Hook["useInitializeMetrics"]
Hook -->|"setClientId / setSessionId"| LS[localStorage]
Hook -->|"tabCount, beforeunload"| LS
Hook -->|"30 min inactivity"| LS
end
subgraph event [Отправка события]
Component -->|"setPageParams"| Store
Component -->|"setEventClick(...)"| Store
Store --> GetDefault["getDefaultAnalyticsData"]
GetDefault --> SetIds["setAnalyticId (YM + Comagic)"]
SetIds --> Push["pushLayer → dataLayer"]
end
subgraph ids [Сбор идентификаторов]
SetIds --> YM["getYmUid"]
SetIds --> CM["getComagicVisitorId"]
GetDefault --> FP["fingerprint_id (store)"]
GetDefault --> Auth["auth_store / user_reg_id"]
GetDefault --> AB["NEXT_PUBLIC_AB_TEST_GROUP"]
endКлючевые сервисы и утилиты
| Экспорт | Файл | Назначение |
| --- | --- | --- |
| stoneDataLayerStore | StoneDataLayerStore.ts | Синглтон MobX-стора, точка входа для всех событий |
| useInitializeMetrics | useInitializeMetrics.ts | Инициализация ID сессии/клиента в корневом провайдере |
| getPageType | pageType.ts | Тип страницы: main, lot_card, project_card, lot_catalog и др. |
| getPageDirection | pageDirection.ts | Направление: apartments, offices, retail, rent, investments |
| getProjectName | getProjectName.ts | Slug проекта из URL |
| getLotName | getLotName.ts | Slug лота из URL |
| getStoneHashClientId | utils.ts | FingerprintJS visitor ID |
| getStoneClientId / getStoneSessionId | utils.ts | Чтение ID из localStorage |
| getGoogleAnalyticsClientId | utils.ts | Client ID из cookie _ga |
| getYandexMetrikaClientId | utils.ts | Client ID из cookie _ym_uid |
| getABCookie | getABCookie.ts | Чтение A/B cookie по имени |
| useComagicStatus | useComagicStatus.ts | React-хук: загружен ли скрипт Comagic |
| getVisibleVirtualPn | getVisibleVirtualPn.ts | MD5-хеш виртуального номера из #header_phone |
| sendAnalyticsToAPI | sendAnalyticsToAPI.ts | POST на бэкенд (не экспортируется из index.ts, доступен в исходниках) |
Схема хранения данных
Пакет не использует серверную БД. Все персистентные данные хранятся в localStorage браузера и в window.dataLayer.
localStorage
erDiagram
st_client_id {
string value "timestamp + random, обновляется раз в год"
}
st_session_id {
string value "timestamp + random, сбрасывается при закрытии последней вкладки или 30 мин неактивности"
}
tabCount {
int value "счётчик открытых вкладок"
}
user_reg_id {
string value "ID зарегистрированного пользователя (читается, не создаётся пакетом)"
}
auth_store {
json value "состояние авторизации { isLoggedIn: boolean }"
}
_ga {
string value "fallback GA client ID"
}
_ym_uid {
string value "fallback YM client ID"
}| Ключ | Тип | Жизненный цикл | Описание |
| --- | --- | --- | --- |
| st_client_id | string | 1 год с момента создания | Уникальный ID клиента. Первые 10 символов — Unix timestamp. Пересоздаётся, если прошёл год (checkTimeStamp) |
| st_session_id | string | На сессию | Уникальный ID сессии. Сбрасывается при tabCount === 0 (закрытие последней вкладки) или после 30 мин бездействия |
| tabCount | string (int) | На сессию | Счётчик открытых вкладок. Инкрементируется при монтировании useInitializeMetrics, декрементируется в beforeunload |
| user_reg_id | string | Управляется приложением | ID пользователя, читается в initializeUserId() |
| auth_store | JSON | Управляется приложением | { isLoggedIn: boolean }, читается в initializeUserAuth() |
Формат UID (makeUID.ts): Date.now() + случайное число 100000000–999999999.
window.dataLayer
Массив объектов событий. GTM читает его через триггеры. Пакет пушит через store.pushLayer():
window.dataLayer = window.dataLayer || []
window.dataLayer.push(tagManagerArgs)Формат события в dataLayer
Каждое событие содержит общие поля из getDefaultAnalyticsData + специфичные поля события:
| Поле | Тип | Источник | Описание |
| --- | --- | --- | --- |
| event | string | событие | Тип: pageview, click, submit, … |
| page_type | string | null | getPageType | Тип страницы |
| page_direction | string | null | getPageDirection | Направление (apartments/offices/…) |
| page_project | string | null | getProjectName | Slug проекта |
| page_lot | string | null | getLotName | Slug лота |
| ym_uid | string | null | getYmUid(95057596) | Client ID Яндекс.Метрики |
| cm_visitor_id | string | null | Comagic.getVisitorId() | ID посетителя Comagic |
| fingerprint_id | string | null | FingerprintJS | Браузерный fingerprint |
| visible_virtual_pn | string | undefined | MD5 #header_phone | Хеш виртуального номера |
| ab_test_name | string | store | Всегда 'abgroup' |
| ab_test_group | string | null | NEXT_PUBLIC_AB_TEST_GROUP | Группа A/B-теста |
| user_id | string | null | user_reg_id | ID пользователя |
| auth | boolean | auth_store | Авторизован ли пользователь |
| widget | object | undefined | вызывающий код | { widget_agent_id, widget_session_id, response_time_ms } |
API
POST /api/analytics/send-event
Внешний эндпоинт бэкенда STONE. Вызывается из sendAnalyticsToAPI.
POST https://stone.ru/api/analytics/send-event
Content-Type: application/jsonТело запроса:
{
"event": "click",
"name": "banner_main",
"payload": {
"st_client_id": "1710000000123456789",
"st_session_id": "1710000000987654321",
"st_client_hash_id": "abc123...",
"ym_client_id": "...",
"ga_client_id": "...",
"project": "sokolniki",
"page_path": "/catalog/residential/sokolniki/lot-1",
"page_url": "https://stone.ru/catalog/residential/sokolniki/lot-1"
}
}| Поле | Тип | Описание |
| --- | --- | --- |
| event | string | Тип события |
| name | string | Имя/идентификатор события |
| payload | object | Параметры + автоматически добавленные page_path, page_url |
Ответ: стандартный ответ бэкенда (пакет не обрабатывает тело ответа).
Поведение:
- Запрос не отправляется, если
NEXT_PUBLIC_IS_TEST === 'true' - Запрос не отправляется на сервере (SSR) — только в браузере
- Ошибки сети логируются в консоль, не пробрасываются
Контракт dataLayer
Основной транспорт событий — window.dataLayer. Каждый метод stoneDataLayerStore.setEvent* формирует объект и вызывает pushLayer.
Общий шаблон:
{
"event": "<тип>",
"page_type": "lot_card",
"page_direction": "apartments",
"page_project": "sokolniki",
"page_lot": "lot-1",
"ym_uid": "123456789",
"cm_visitor_id": "cm-abc",
"fingerprint_id": "fp-xyz",
"visible_virtual_pn": "a1b2c3...",
"ab_test_name": "abgroup",
"ab_test_group": "A",
"user_id": null,
"auth": false
}Типы событий
pageview
stoneDataLayerStore.setEventPageView({ auth?, widget? })Просмотр страницы. Не включает ym_uid в payload (удаляется перед push).
pageview_force
stoneDataLayerStore.setEventPageViewForce()Тестовая отправка pageview без ожидания загрузки внешних ID.
click
stoneDataLayerStore.setEventClick({
action_element?, action_element_text?, block_name?,
clicked_url?, clicked_lot?, clicked_lot_project?,
clicked_lot_position?, action_element_status?,
banner_position?, banner_text?, auth?, widget?
})Клик по элементу. Текст очищается через cleanString (удаление , <br>, лишних пробелов).
submit
stoneDataLayerStore.setEventSubmit({
lead_id, block_name?, submitted_lot?,
submitted_lot_project?, auth?, widget?
})Отправка формы / создание лида.
lot_feed_show
stoneDataLayerStore.setEventLotFeedShow({
block_name?, list_of_lots: [{ position, lot, lot_project }],
widget?
})Показ ленты лотов. Не включает ym_uid и cm_visitor_id.
check
stoneDataLayerStore.setEventCheck({
action_element, action_element_text, block_name?, auth?, widget?
})Чекбокс / переключатель.
blur
stoneDataLayerStore.setEventBlur({
action_element, action_element_status: 'error' | 'success',
block_name?, auth?, widget?
})Потеря фокуса поля формы (валидация).
banner_show / banner_click
stoneDataLayerStore.setEventBannerShow({ banner_text, banner_link, banner_position?, block_name?, auth?, widget? })
stoneDataLayerStore.setEventBannerClick({ banner_text, banner_link, clicked_url?, block_name?, auth?, widget? })Показ и клик по баннеру.
onboarding_show / onboarding_close
stoneDataLayerStore.setEventOnboardingShow({ banner_text, banner_link, banner_position?, block_name?, auth?, widget? })
stoneDataLayerStore.setEventOnboardingClose({ banner_text, banner_link, banner_position?, block_name?, auth?, widget? })Показ и закрытие онбординга.
chat
stoneDataLayerStore.setEventChat({
action_element, action_element_text, message?,
block_name?, auth?, widget?
})Действия в чате.
fullscreenview
stoneDataLayerStore.setEventFullScreen({ auth?, widget? })Просмотр в полноэкранном режиме.
Сводная таблица событий
| Метод store | event | YM/Comagic ID | visible_virtual_pn | Специфичные поля |
| --- | --- | --- | --- | --- |
| setEventPageView | pageview | — | — | auth, widget |
| setEventPageViewForce | pageview_force | — | — | auth, widget |
| setEventClick | click | + | + | action_element, clicked_url, clicked_lot, banner_* |
| setEventSubmit | submit | + | + | lead_id, submitted_lot, submitted_lot_project |
| setEventLotFeedShow | lot_feed_show | — | + | list_of_lots, block_name |
| setEventCheck | check | + | + | action_element, action_element_text |
| setEventBlur | blur | + | + | action_element, action_element_status |
| setEventBannerShow | banner_show | + | — | banner_text, banner_link, banner_position |
| setEventBannerClick | banner_click | + | — | banner_text, banner_link, clicked_url |
| setEventOnboardingShow | onboarding_show | + | — | banner_text, banner_link, banner_position |
| setEventOnboardingClose | onboarding_close | + | — | banner_text, banner_link, banner_position |
| setEventChat | chat | + | — | action_element, action_element_text, message |
| setEventFullScreen | fullscreenview | + | + | auth, widget |
Сценарии использования
Подключение в Next.js-приложении
sequenceDiagram
participant Dev as Разработчик
participant App as AppProvider
participant Hook as useInitializeMetrics
participant LS as localStorage
participant Page as Страница
participant Store as stoneDataLayerStore
participant DL as dataLayer
Dev->>App: npm i stone-analytics
Dev->>App: useInitializeMetrics() в корневом провайдере
Hook->>LS: setClientId() / setSessionId()
Page->>Store: setPageParams({ pathname, source, href })
Page->>Store: setEventPageView()
Store->>DL: pushLayer(event)import { useInitializeMetrics, stoneDataLayerStore } from 'stone-analytics'
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
useInitializeMetrics()
return <>{children}</>
}
// На странице:
stoneDataLayerStore.setPageParams({
pathname: '/catalog/residential/sokolniki/lot-1',
source: 'stone',
href: window.location.href,
})
await stoneDataLayerStore.setEventPageView({ auth: false })Пайплайн отправки события
flowchart TD
Start([Вызов setEventClick]) --> PageParams{"pageParamsWasSet?"}
PageParams -->|нет| Warn["page_type/direction/project/lot = undefined"]
PageParams -->|да| Default["getDefaultAnalyticsData(store)"]
Warn --> Default
Default --> Enrich["setAnalyticId: ym_uid + cm_visitor_id"]
Enrich --> VirtualPn["visible_virtual_pn = getVisibleVirtualPn()"]
VirtualPn --> IsTest{"NEXT_PUBLIC_IS_TEST === 'true'?"}
IsTest -->|да| Skip["Событие не пушится"]
IsTest -->|нет| Push["pushLayer → window.dataLayer"]
Push --> GTM["GTM-триггеры обрабатывают событие"]Кейс: просмотр страницы
- Пользователь открывает
/catalog/residential/sokolniki/lot-42 - Приложение вызывает
setPageParams({ pathname, source: 'stone', href }) getPageType→lot_card,getPageDirection→apartments,getProjectName→sokolniki,getLotName→lot-42- Приложение вызывает
setEventPageView({ auth: false }) - В
dataLayerпушится объект сevent: 'pageview'и контекстом страницы - GTM отправляет данные в подключённые системы аналитики
Кейс: клик по лоту в каталоге
- Пользователь кликает на карточку лота в блоке
catalog_grid - Компонент вызывает:
await stoneDataLayerStore.setEventClick({
block_name: 'catalog_grid',
action_element: 'lot_card',
action_element_text: 'Квартира 45 м²',
clicked_lot: 'lot-42',
clicked_lot_project: 'sokolniki',
clicked_lot_position: '3',
auth: false,
})- Текст очищается от HTML-сущностей
- Добавляются
ym_uid,cm_visitor_id,visible_virtual_pn - Событие
clickпопадает вdataLayer
Кейс: отправка формы (submit)
- Пользователь отправляет форму обратной связи
- Бэкенд возвращает
lead_id - Компонент вызывает:
await stoneDataLayerStore.setEventSubmit({
lead_id: '12345',
block_name: 'contact_form',
submitted_lot: 'lot-42',
submitted_lot_project: 'sokolniki',
auth: true,
})- В
dataLayerпушитсяevent: 'submit'сlead_id
Кейс: истечение сессии
Закрытие последней вкладки:
beforeunloadдекрементируетtabCount- Если
tabCount === 0→st_session_idочищается - При следующем визите
useInitializeMetricsсоздаёт новыйst_session_id
Неактивность 30 минут:
- Таймер сбрасывается при
mousemove,click,touchstart,scroll,keydown - По истечении 30 мин →
st_session_idочищается - Следующее событие получит новый session ID при перезагрузке
Кейс: тестовый режим (события не отправляются)
- В
.env.localприложения-потребителя:NEXT_PUBLIC_IS_TEST=true pushLayer,dataLayerPush,sendAnalyticsToAPI,ymTarget— все проверяют этот флаг- События формируются, но не отправляются ни в dataLayer, ни на API, ни в Яндекс.Метрику
- Удобно для локальной разработки без засорения аналитики
Установка
В проекте-потребителе (Next.js / React)
npm i stone-analytics@latestТребуется React 18+ (peer-зависимость, не устанавливается автоматически).
Локальная разработка пакета
git clone <repo-url> stone-analytics
cd stone-analytics
npm installЗапуск
Разработка пакета
# Сборка с watch-режимом (tsup)
npx tsup --watch
# Одноразовая сборка
npm run buildИспользование в приложении-потребителе
# В .env.local приложения
NEXT_PUBLIC_IS_TEST=true # отключить отправку событий
NEXT_PUBLIC_AB_TEST_GROUP=A # группа A/B-теста
NEXT_PUBLIC_YM_COUNTER=95057596 # счётчик Яндекс.Метрики (для ymTarget)// app/providers.tsx
import { useInitializeMetrics } from 'stone-analytics'
export function Providers({ children }) {
useInitializeMetrics()
return children
}Команды
| Команда | Описание |
| --- | --- |
| npm run build | Сборка в dist/ (CJS + .d.ts, minify, sourcemap) |
| npm run version-update | Инкремент patch-версии в package.json |
| npm run commit-and-push | Коммит и push с сообщением Update version to X.Y.Z |
| npm run release | Полный цикл: build → version-update → commit-and-push → npm publish --tag latest |
| npm version (lifecycle) | Автоматически запускает build и git add . |
Развертывание
Зависимости потребителя
Для полной работы аналитики на сайте-потребителе необходимы:
| Зависимость | Назначение |
| --- | --- |
| React 18+ | Хуки useInitializeMetrics, useComagicStatus |
| Google Tag Manager | Скрипт GTM на странице, читает window.dataLayer |
| Яндекс.Метрика | Скрипт ym, счётчик 95057596 (hardcoded в getYmUid) |
| Comagic | Скрипт Comagic для getVisitorId() |
| Элемент #header_phone | <a id="header_phone" href="tel:..."> для visible_virtual_pn |
Публикация в npm
# Полный релиз (build + bump version + commit + push + publish)
npm run release
# Или по шагам:
npm run build
npm run version-update
npm publish --tag latestПакет публикуется как stone-analytics с "access": "public". Точки входа:
| Формат | Путь |
| --- | --- |
| CJS | dist/index.cjs |
| ESM | dist/index.js |
| Types | dist/index.d.ts |
prepublishOnly автоматически запускает build перед публикацией.
Переменные окружения
Переменные задаются в приложении-потребителе (Next.js), не в самом пакете:
| Переменная | Описание | Значение по умолчанию |
| --- | --- | --- |
| NEXT_PUBLIC_IS_TEST | Тестовый режим: отключает отправку во все транспорты | — (отправка включена) |
| NEXT_PUBLIC_AB_TEST_GROUP | Группа A/B-теста, попадает в ab_test_group | — |
| NEXT_PUBLIC_YM_COUNTER | ID счётчика Яндекс.Метрики для ymTarget | — |
Интеграция с внешними системами
| Система | Механизм | ID счётчика / конфиг |
| --- | --- | --- |
| Google Tag Manager | window.dataLayer.push() | Настраивается на стороне GTM |
| Яндекс.Метрика | getYmUid(95057596) в событиях; ymTarget → reachGoal | 95057596 (hardcoded) |
| Comagic | Comagic.getVisitorId() | Скрипт Comagic на странице |
| Google Analytics | getGoogleAnalyticsClientId() из cookie _ga | Cookie GA |
| FingerprintJS | getStoneHashClientId() → fingerprint_id | Open-source FingerprintJS |
| Бэкенд STONE | POST stone.ru/api/analytics/send-event | Без авторизации на стороне клиента |
Источники страниц (TSource): stone, sokolniki, stone-hod, stonehod, realty — влияют на маппинг page_type в pageType.ts.
Чек-лист
Перед публикации новой версии проверьте:
- [ ]
npm run buildпроходит без ошибок,dist/актуален - [ ] Типы в
dist/index.d.tsсоответствуют экспортам изsrc/index.ts - [ ] Версия в
package.jsonинкрементирована - [ ]
NEXT_PUBLIC_IS_TEST=trueработает — события не уходят в dataLayer/API/YM - [ ]
useInitializeMetricsкорректно создаёт/обновляетst_client_idиst_session_id - [ ]
setPageParamsвызывается доsetEvent*на каждой странице - [ ] Все 13 типов событий (
setEvent*) формируют корректный payload - [ ] README актуален: версия пакета, список экспортов, переменные окружения
- [ ] Примеры кода в README компилируются и соответствуют API
- [ ]
npm publishвыполнен с тегомlatest
