Вероятно, какие-то вещи я повторю снова – т.к сложно избежать желания давать советы.
Важно! Предполагается, что читатель обладает базовыми знаниями и уже пробовал работать с Amplitude и Google Tag Manager
Проблемы аналитики одностраничных веб-приложений
Некоторые продуктовые менеджеры или аналитики привыкли к тому, что по веб-сайтам базовая аналитика собирается из коробки. В первую очередь речь идет про переходы между страницами. Всё, что вы увидите в Google Analytics для страниц на Angular или React – это будет название вашего сайта (как правило это и будет заголовком первой страницы).
Увы, из коробки ничего полезного собираться не будет. Но если вы когда-либо работали с нативными мобильными приложениями, то сложностей с понимаем основных принципов не будет. Хотите получить данные – их нужно разметить (с командой разработки).
Т.к нам все равно все размечать руками, то мой выбор в пользу Amplitude. В следующий раз попробую описать, что можно сделать и с GA4.
Зачем GTM и почему бы не использовать его вместо разработки?
Google Tag Manager полезный инструмент, даже не смотря на ограничения в работе на SPA. Сложность в том, что если проект живой и в разработке, то гарантированно разметить триггеры в GTM (для SPA) не получится - не буду лукавить, есть теги, которые за 6 месяцев жизни проекта действительно не менялись.
За полгода из работающих событий в Amplitude созданных с помощью обычных механизмов GTM (когда вы размечаете триггеры самостоятельно, например, по селекторам - без разработки) осталось не более 30%
Да, селекторы в Angular (и любом другом SPA) тоже часто меняются. Даже название поля ввода - mat-input-0 завтра может стать mat-input-1, если в проекте появились новые поля ввода. SPA вообще не гарантирует нейминг селекторов - хотя тут скорее всего со мной кто-то может поспорить.
Тогда зачем GTM?
- С его помощью я запускаю разные конфигурации Amplitude на разных контурах для теста и для боя (нет смысла заставлять разработчиков решать такую задачу)
- Добавляю свойства событий и пользователей (например, браузер пользователя или размеры разрешения браузера). Много всего, у всех задачи разные
- Проверяю локальные проблемы добавляя временно новые события с помощью селекторов или других идентификаторов
и т.д
Т.е GTM передает события в Amplitude и это удобно, потому что данные можно модифицировать.
Код в поле HTML для тега конфигурации взять из кабинета Amplitude - JavaScript SDK.
Из необычного в проекте сделан кастомный триггер, который по доменному имени запускает разные конфигурации проектов в Амплитуде.
Саму проверку делаю в переменной
Переменная всегда должна что-то возвращать. Код не самый красивый - т.к часто преобразовывался, но тут скорее про смысл (домен конечно заменен).
С этой переменной работает Триггер на инициализацию с условием активации тега на значение переменной выше (триггера 3 - на 3 разных контура).
Дальше самое важное: фронт шлет пуш-события в data-слой по интересующим вас событиям. Для разработки, это будет что-то вроде:
"window.dataLayer.push({'event':'<название ивента из таблички>', 'property1':'<название свойства события из таблички>', 'property2':'<значения свойства события из таблички>'})
Свойства property1 и property2 из примера вы можете назвать как угодно, но именно с этими именами вам нужно будет создать переменные в GTM чтобы передать их в события. Их может быть столько, сколько вам нужно.
Если разработка все сделала правильно, то в консоли GTM появятся события с неймингом из поля event, свойства этих событий можно посмотреть переключившись на вкладку Data Layer
В фигурных скобках ссылки на созданные переменные в GTM.
Если вам не нужна кастомизация под разные события, то вместо имени события 'MOE_SOBITIE_DLYA_AMPLITUDE' можно написать {{Event}} (так же заведите его, как переменную) - тогда нейминг ваших событий от разработки сразу будет прокидываться в Амплитуду.
Коллеги массово пишут не использовать много свойств событий, но я могу сказать, что именно по свойствам иногда получается находить ошибки - когда у тебя прилетает экран для авторизованного пользователя, а все свойства говорят об обратном.
Ну и наш Тег (теги) с пользовательским HTML должны запускаться по Триггеру "Специальные события"
Где в поле "Название события" указано значение поле Event от разработки, т.е то значение, что отображает консоль GTM.
Если у вас один Тег с именем {{Event}}, которое передается в Amplitude для всех событий, как я писал выше, то у него будет множество Триггеров под каждое событие. Не забывайте добавлять новые Триггеры с появлением новых ивентов от разработки.
Строчка с amplitude.getInstance().setUserProperties({'data': eventProperties.data}); скорее всего в рамках этой статьи вам не нужна - она присваивает свойства пользователю. Я думаю расскажу об этих особенностях в другой раз.
Полезные переменные
К сожалению, авторство указать сейчас для меня крайне сложно, но думаю кому важно загуглит. Точно помню, что много взял, например, тут:
Например, так выглядит парсинг ЮТМ меток (чтобы так же записать)
Из полезного - метку не перехватить, если в SPA на запуске сразу срабатывает редирект. Надо либо передавать в рекламу конечную страницу, либо с разработкой делать проброс меток.
Вот так в переменную забрать свойства события, для каждого свойства - своя переменная
Если вам не нужна кастомизация под разные события, то вместо имени события 'MOE_SOBITIE_DLYA_AMPLITUDE' можно написать {{Event}} (так же заведите его, как переменную) - тогда нейминг ваших событий от разработки сразу будет прокидываться в Амплитуду.
Коллеги массово пишут не использовать много свойств событий, но я могу сказать, что именно по свойствам иногда получается находить ошибки - когда у тебя прилетает экран для авторизованного пользователя, а все свойства говорят об обратном.
Если у вас один Тег с именем {{Event}}, которое передается в Amplitude для всех событий, как я писал выше, то у него будет множество Триггеров под каждое событие. Не забывайте добавлять новые Триггеры с появлением новых ивентов от разработки.
Строчка с amplitude.getInstance().setUserProperties({'data': eventProperties.data}); скорее всего в рамках этой статьи вам не нужна - она присваивает свойства пользователю. Я думаю расскажу об этих особенностях в другой раз.
К сожалению, авторство указать сейчас для меня крайне сложно, но думаю кому важно загуглит. Точно помню, что много взял, например, тут:
"}},{"type":"link","cover":false,"hidden":false,"anchor":"","data":{"link":{"type":"link","data":{"url":"https://api.vc.ru/v2.8/redirect?to=https%3A%2F%2Fosipenkov.ru%2Fanalytics%2F%3Fref%3Dvc.ru&postId=268754","title":"Статьи по веб-аналитике","description":"Статьи по веб-аналитике, работа с инструментами Google Analytics, Google Tag Manager, Яндекс.Метрика, Google Data Studio, Microsoft Power BI и Excel","image":{"type":"image","data":{"uuid":"7a16a6d5-01eb-5151-83ba-4dab1bf93cd1","width":300,"height":300,"size":6002,"type":"png","color":"29c1e0","hash":"","external_service":[]}},"v":1,"hostname":"osipenkov.ru"}}}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"
Название переменных должно быть доступным и легко читаемым - именно к нему вы будете обращаться в Тегах в фигурных скобках.
Из полезного - метку не перехватить, если в SPA на запуске сразу срабатывает редирект. Надо либо передавать в рекламу конечную страницу, либо с разработкой делать проброс меток.
Вот так в переменную забрать свойства события, для каждого свойства - своя переменная
"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"0b190335-589e-5184-ae13-7607953a370c","width":942,"height":303,"size":19408,"type":"png","color":"e4e6e6","hash":"","external_service":[]}}}]}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":2,"favorites":17,"reposts":0,"views":27,"hits":1964,"reads":null,"online":0},"dateFavorite":0,"hitsCount":1964,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/services/268754-produktovaya-analitika-v-odnostranichnom-veb-prilozhenii-spa-chast-1","author":{"id":150132,"name":"Dmitry Sedin","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"38f53f44-594f-3669-8f9e-c737a4b57d6a","width":0,"height":0,"size":1,"type":"jpg","color":"","hash":"","external_service":[]}},"cover":null,"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":5271790,"userId":150132,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5271790"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1647703,"userId":150132,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1647703"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":420995,"userId":150132,"count":0,"shareImage":"https://api.vc.ru/achievements/share/420995"}],"lastModificationDate":1765084698,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":200396,"name":"Сервисы","description":"Новые сервисы, обновления инструментов, опыт использования и полезные приёмы.","uri":"/services","avatar":{"type":"image","data":{"uuid":"158fab2d-76c1-5ed8-898a-76ee48d4c795","width":1200,"height":1200,"size":99571,"type":"png","color":"7cdaea","hash":"08183848d81000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"c3fe3abb-6808-527e-9eb1-2b6bb92ea400","width":3840,"height":1120,"size":19502,"type":"png","color":"7cdcec","hash":"","external_service":[]}},"lastModificationDate":1688995401,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"services","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":4}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null,"keywords":[],"media":null,"customCover":null,"robotsTag":null,"categories":[],"isAnonymized":true}};