Как работают популярные счетчики веб или мобильной аналитики, например, Google Analytics или AppsFlyer? На сайт устанавливаются их коды или в приложение интегрируется мобильное SDK. Потом при каждом действии клиента отправляется http запрос на сервер аналитики. Например, для Google Analytics в простейшем случае это будет: https://analytics.google.com/g/collect?v=2&tid=G-XXXXXXXXXX&cid=1415663337.1642776362&en=order
, где:
tid - уникальный идентификатор потока(счетчика)
cid - уникальный идентификатор браузера
en - название события
У использования стандартных счетчиков/пикселей есть минусы:
некоторые посетители используют анонимайзеры, которые блокируют такие запросы;
их сложно кастомизировать под себя.
В этой статье напишем собственный счетчик, который будет решать эти проблемы. Счетчик встроим в PowerBI отчеты. Но принцип одинаков, его можно будет использовать и на веб-сайте, и в приложении, и в других устройствах с доступом к интернету. Попробуем две точки сбора событий, чтобы изучить больше технологий: Google Cloud Function, которая будет писать события в Google BigQuery, и Amazon Lambda Functions с записью событий в Snowflake.
1) Заходим на страницу Google Cloud Function - Create function. Убеждаемся, что установлен триггер HTTP, не установлена галочка напротив Require authentication. Желательно, чтобы Region совпадал с регионом в вашем датасете в BigQuery.
URL на скриншоте – это тот адрес, куда нужно будет отправлять события. Например, https://us-central1-project-name.cloudfunctions.net/powerbi
Через GET параметры можно передавать любые данные. Например, при использовании на веб-сайте будет полезно передавать referrer. В нашем же случае это будет название отчета в PowerBI и имя страницы. Тогда адрес для отправки будет иметь вид: https://us-central1-project-name.cloudfunctions.net/powerbi?report=MyReport&page=Page1
2) Далее попадаем на страницу редактирования кода функции. Выбираем Python. В файле requirements.txt прописываем библиотеки, которые нужно подгрузить в проект:
requests>=2.27.1
google.cloud>=0.34.0
google-cloud-bigquery>=2.32.0
В веб-интерфейсе создаем файл config.py следующего содержания:
# id чата в телеграме куда будут отправляться ошибки. чтобы узнать свой id - напишите боту @userinfobot
tg_chat_id = 'XXXXXXX'
# токен бота. чтобы его получить, создайте бота написав @BotFather
tg_bot_token = 'YYYYYYY:YYYYYYYYYYYYYYYYYYYYYYYYYY'
# название таблицы в BigQuery куда будут писаться события. Ее нужно создать заранее.
bq_table_name = 'project-name.dataset.powerbi_views'
Файл main.py:
import datetime
import requests
from google.cloud import bigquery
from config import tg_chat_id, tg_bot_token, bq_table_name
def main(request, report='', page='', ip='', user_agent=''):
if 'report' in request.args:
report = request.args['report']
if 'page' in request.args:
page = request.args['page']
#for headers in request.headers: header = header + str(headers) # получить все заголовки
ip = request.headers['X-Forwarded-For']
user_agent = request.headers['User-Agent']
if report != '':
return stream_bq(report, page, ip, user_agent)
else:
url = 'https://api.telegram.org/bot' + tg_bot_token + '/sendMessage?chat_id=' + tg_chat_id + '&text=google cloud functions powerbi error: event without parameters received'
response = requests.get(url)
return 'event without parameters received'
def stream_bq(report, page, ip, user_agent):
bq_client = bigquery.Client()
bq_table = bq_client.get_table(bq_table_name)
row = [{'report': report,
'page': page,
'ip': ip,
'timestamp': datetime.datetime.utcnow(),
'user_agent': user_agent }]
bq_client.insert_rows(bq_table, row)
return 'ok'
Нажимаем Deploy, проверяем работу: открываем в браузере URL из пункта 1. Должны увидеть текст “ок”. Если это не так – ищем ошибки в Logs.
3) В BigQuery данные будут выглядеть следующим образом:
Далее обогатим эти данные:
3.1) По ip определим страну, город, почтовый индекс, используя данные GeoLite2 Free Geolocation Data:
SELECT *
FROM ( SELECT ip FROM `project-name.dataset.powerbi_views` GROUP BY ip ) AS q
CROSS JOIN
(
SELECT country_name, network, network_bin, city_name, postal_code, mask
FROM `fh-bigquery.geocode.201806_geolite2_city_ipv4_locs`
) AS g
WHERE network_bin = NET.IP_FROM_STRING(ip) & NET.IP_NET_MASK(4, mask)
Результат будет выглядеть примерно так:
3.2) Из user_agent выделим операционную систему и браузер, используя JS библиотеку woothee:
CREATE OR REPLACE FUNCTION `project-name.dataset.decode_user_agent`(ua STRING)
RETURNS STRING
LANGUAGE js AS """ return JSON.stringify(woothee.parse(ua));
"""OPTIONS(library="gs://project-name-bucket/woothee.js");
SELECT JSON_VALUE(json, '$.os') AS os, JSON_VALUE(json, '$.name') AS browser, json, user_agent,
FROM
(
SELECT user_agent, `bproject-name.dataset.decode_user_agent`(user_agent) AS json
FROM `bi-analytics-318415.Others.powerbi_views`
GROUP BY user_agent
)
Результат:
3.3) Теперь мы знаем не только названия отчетов и страницы, которые смотрят пользователи, но и информацию о местоположении, устройстве и операционную систему. Можно сформировать отчетность на свой вкус.
4) Интегрируем наш счетчик в PowerBI. Для этого в редакторе создаем таблицу с единственным значением – нашим URL:
Затем выбираем эту колонку справа и в меню Column tools указываем Data category = Image URL:
После этого вставляем в отчет визуальный элемент “таблица” с этой колонкой где-нибудь в малозаметном углу отчета. Теперь при каждом его открытии будет уходить http запрос, который через Cloud Functions будет фиксироваться в BigQuery. Наш счетчик готов!
5) Чтобы не быть ограниченным одним облачным вендором, проделаем аналогичное на AWS Lambda. В качестве хранилища будем использовать Snowflake, но вы можете использовать любое, например, Redshift. Скорость записи (по логам) в таком случае будет медленней чем в первом примере Google Cloud Function -> Google BigQuery. Но у нас еще побочная цель – изучить разные подходы, чтобы каждый мог для себя выбрать оптимальный вариант.
Заходим в AWS Lambda. Создаем новую функцию powerbi . Вариант создания – “Start with a simple Hello World example”. Runtime – указываем версию Python, которая установлена у вас на локальном компьютере, например, Python 3.8. Далее нужно добавить триггер типа API Gateway - HTTP API. Security указываем Open. После выполненных действий в консоли, функция должна выглядеть примерно так:
URL для отправки событий будет иметь вид: https://your-id.execute-api.us-east-2.amazonaws.com/default/powerbi?report=MyReport&page=Page1
По умолчанию Timeout для функции (время ее работы) составляет 3 секунды. Для нас этого недостаточно, т.к. будем писать во внешний сервис Snowflake. Особенно много времени нужно, если события будут приходить редко, т.к. много времени уходит на запуск Warehouse в Snowflake после его остановки. Заходим в Configuration -> General configuration -> Edit -> Timeout = 12 sec.
6) Python код нашей функции будет использовать библиотеки requests и snowflake.connector. В AWS Lambda библиотеки подключаются не так как в Google Cloud Functions. Поэтому сначала создадим проект на локальном компьютере, потом загрузим в Lambda zip-архив. Инструкция. По моему опыту, самым оптимальным является вариант Using a virtual environment.
На локальном компьютере создаем пустую папку, где будут лежать файлы проекта.
Создаем файл config.py следующего содержания:
# id чата в телеграме куда будут отправляться ошибки. чтобы узнать свой id - напишите боту @userinfobot
tg_chat_id = 'XXXXXXX'
# токен бота. чтобы его получить, создайте бота написав @BotFather
tg_bot_token = 'YYYYYYY:YYYYYYYYYYYYYYYYYYYYYYYYYY'
# ниже реквизиты доступа от snowflake. таблицу нужно создать заранее
sn_user = 'user_name'
sn_password = 'user_password'
sn_account = 'youproject.us-east-2.aws'
sn_database = 'test'
sn_schema = 'PUBLIC'
sn_table = 'powerbi_views'
sn_warehouse = 'COMPUTE_WH'
sn_role_name = 'ACCOUNTADMIN'
Создаем файл lambda_function.py :
import datetime
import json
import requests
import snowflake.connector
from config import tg_chat_id, tg_bot_token, sn_user, sn_password, sn_account, \
sn_warehouse, sn_database, sn_table, sn_schema
def lambda_handler(event={}, context='', report='', page='', ip='', user_agent=''):
if event.get('queryStringParameters') is not None:
if 'report' in event['queryStringParameters']:
report = event['queryStringParameters']['report']
if 'page' in event['queryStringParameters']:
page = event['queryStringParameters']['page']
ip = event['headers']['x-forwarded-for']
user_agent = event['requestContext']['http']['userAgent']
time = datetime.datetime.utcnow()
return stream_snowflake(report, page, ip, time, user_agent)
else:
url = 'https://api.telegram.org/bot' + tg_bot_token + '/sendMessage?chat_id=' + tg_chat_id + '&text=aws lambda powerbi error: event without parameters received. event = ' + str(event)
response = requests.get(url)
return 'event without parameters received'
def stream_snowflake(report, page, ip, time, user_agent):
error = False
try:
conn = snowflake.connector.connect(user=sn_user, password=sn_password, account=sn_account, warehouse=sn_warehouse,
database=sn_database, schema=sn_schema)
cs = conn.cursor()
cs.execute(
"INSERT INTO " + sn_table + "(report, page, ip, time, user_agent) VALUES " +
" (%s, %s, %s, %s, %s) ", (report, page, ip, time, user_agent))
except Exception as e:
error = True
url = 'https://api.telegram.org/bot' + tg_bot_token + '/sendMessage?chat_id=' + tg_chat_id + '&text=aws lambda powerbi error:' + str(e)
response = requests.get(url)
finally:
try:
cs.close()
except Exception as u:
pass
if error == False:
return 'Write in snowflake successfully'
else:
return 'Was error with Snowflake. Send message to telegram'
# это код для запуска на локальном компьютере
# if __name__== "__main__":
# lambda_handler()
Проверим на локальном компьютере, что наш скрипт работает. Для этого нужно раскомментировать блок снизу и в функции lambda_handler прописать вызов stream_snowflake без условий (то есть с пустыми значениями report и т.д.) Если все норм, загружаем наш проект в AWS Lambda по инструкции в начале этого пункта.
Открываем в браузере URL из пункта 5 для запуска Lambda функции, проверяем что все работает. Логи находятся в Monitor -> Logs.
7) В Snowflake собираемы данные должны выглядеть следующим образом:
Далее обогатим эти данные:
7.1) Стандартного общедоступного набора данных по geo-ip в Snowflake нет (или я не нашел). Но на Snowflake Data Marketplace есть возможность получить 2-х недельный триал к IPINFO: IP GEOLOCATION . После получения доступа скопируем данные себе. После окончания триала они перестанут обновляться. На мою первую заявку не было никакой реакции, поэтому пришлось зарегистрировать второй аккаунт – с ним все получилось.
После получения доступа, если мы попробуем скопировать таблицы командой CLONE - но там покажется ошибка: SQL compilation error: Cannot clone from a table that was imported from a share.
Поэтому скопируем двумя командами:
CREATE TABLE test.public.location_ip LIKE ipinfo_snowflake_myproject_trial.public.location;
INSERT INTO test.public.location_ip SELECT * FROM ipinfo_snowflake_myproject_trial.public.location;
Далее для получения региона и индекса (не влез на скриншот) можно использовать этот SQL-запрос:
SELECT *
FROM
(
SELECT ip, PARSE_IP(ip, 'inet'):ipv4 AS ipv4
FROM ( SELECT ip FROM test.public.powerbi_views WHERE ip='X.X.X.X' GROUP BY ip )
) AS q
LEFT JOIN test.public.location_ip AS l ON q.ipv4<=l.end_ip_int AND q.ipv4>=l.start_ip_int
7.2) Для парсинга user_agent не получиться использовать JavaScript UDF функцию, т.к. в Snowflake они не поддерживаются. Поддержка Java UDF функций экспериментальна и доступна только для проектов на AWS. Будем использовать библиотеку Yauaa.
Перейдем в БД TEST и создадим Stage. Назовем его, например, YAUAA:
В случае, если выбрали Snowflake Managed, то загрузить файл туда можно с помощью SnowSQL (CLI Client) . Неочевидный момент: в файле ./snowflake/config region нужно указывать вместе с вендором, например us-east-2.aws. После запуска утилиты файл можно загрузить командой PUT: put file:///home/anton/yauaa-snowflake-6.9-udf.jar @YAUAA;
Проверим командой: LIST @YAUAA;
Теперь можно создать UDF функцию. Будьте внимательны, директива imports регистрозависима:
use test;
create or replace function parse_useragent(useragent VARCHAR)
returns object
language java
imports = ('@YAUAA/yauaa-snowflake-6.9-udf.jar')
handler = 'nl.basjes.parse.useragent.snowflake.ParseUserAgent.parse';
SQL-код для определения операционной системы и браузера:
SELECT
parse_useragent(user_agent):AgentName::string AS browser,
parse_useragent(user_agent):OperatingSystemName::string AS os,
user_agent
FROM ( SELECT user_agent FROM test.public.powerbi_views GROUP BY user_agent )
Таким образом мы реализовали в Snowflake такой же функционал, как в BigQuery в пунктах 3 - 4. Выбор за вами. Счетчик можно интегрировать не только в PowerBI, но и в другие BI решения, установить на сайт, в приложение или программу.