fix(generator): сохранена фактура визита при восстановлении
- Зачем: - визит после восстановления не должен менять браузер и источники перехода внутри одного click_id. - Что: - добавлен base_click_id в state v3 для восстановления донора фактуры. - исправлено восстановление timestamp offset без потери микросекунд. - расширены тесты и стыковая проверка browser/source и device/os/geo. - Проверка: - uv run --with-requirements generator/requirements.txt pytest generator/tests -q. - bash -n scripts/check_generated_analytics.sh. - git diff --cached --check.
This commit is contained in:
@@ -12,6 +12,8 @@ CLICKHOUSE_PASSWORD="${CLICKHOUSE_PASSWORD:-123456}"
|
||||
GEN_MODEL_T0="${GEN_MODEL_T0:-2026-01-01T00:00:00+00:00}"
|
||||
GEN_MODEL_T_END="${GEN_MODEL_T_END:-2026-01-01T06:00:00+00:00}"
|
||||
REQUIRE_SUPERSET="${REQUIRE_SUPERSET:-1}"
|
||||
CHECK_LIVE_SEAM="${CHECK_LIVE_SEAM:-auto}"
|
||||
GEN_LIVE_CHECK_MINUTES="${GEN_LIVE_CHECK_MINUTES:-10}"
|
||||
|
||||
fail() {
|
||||
echo "Ошибка: $*" >&2
|
||||
@@ -46,6 +48,13 @@ pg_query() {
|
||||
CH_MODEL_T0="$(clickhouse_datetime_literal "${GEN_MODEL_T0}")"
|
||||
CH_MODEL_T_END="$(clickhouse_datetime_literal "${GEN_MODEL_T_END}")"
|
||||
|
||||
[[ "${GEN_LIVE_CHECK_MINUTES}" =~ ^[0-9]+$ ]] \
|
||||
|| fail "GEN_LIVE_CHECK_MINUTES должен быть целым числом минут"
|
||||
case "${CHECK_LIVE_SEAM}" in
|
||||
0|1|auto) ;;
|
||||
*) fail "CHECK_LIVE_SEAM должен быть 0, 1 или auto" ;;
|
||||
esac
|
||||
|
||||
echo "=== Проверка ClickHouse: данные генерации в DM ==="
|
||||
|
||||
stats_query="
|
||||
@@ -261,6 +270,181 @@ echo "confirmation=${contains_confirmation}"
|
||||
echo "monotonic_ok=${contains_monotonic_ok}"
|
||||
echo "confirmation_share=${contains_confirmation_share}"
|
||||
|
||||
should_check_live_seam="${CHECK_LIVE_SEAM}"
|
||||
if [[ "${should_check_live_seam}" == "auto" ]]; then
|
||||
live_rows_query="
|
||||
WITH
|
||||
toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
|
||||
t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end
|
||||
SELECT count()
|
||||
FROM dds.event
|
||||
WHERE event_ts >= t_end AND event_ts < t_live_end
|
||||
FORMAT TabSeparated"
|
||||
live_rows="$(ch_query "${live_rows_query}")"
|
||||
[[ "${live_rows}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать live-строки DDS после GEN_MODEL_T_END"
|
||||
if (( live_rows > 0 )); then
|
||||
should_check_live_seam="1"
|
||||
else
|
||||
should_check_live_seam="0"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "${should_check_live_seam}" == "1" ]]; then
|
||||
echo ""
|
||||
echo "=== Проверка ClickHouse: стык backfill/live в DDS ==="
|
||||
|
||||
seam_duplicates_query="
|
||||
WITH
|
||||
toDateTime64('${CH_MODEL_T0}', 6) AS t0,
|
||||
toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
|
||||
t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end
|
||||
SELECT
|
||||
count() AS events,
|
||||
uniqExact(event_id) AS unique_events,
|
||||
events - unique_events AS duplicate_events
|
||||
FROM dds.event
|
||||
WHERE event_ts >= t0 AND event_ts < t_live_end
|
||||
FORMAT TabSeparated"
|
||||
|
||||
seam_duplicates="$(ch_query "${seam_duplicates_query}")"
|
||||
IFS=$'\t' read -r seam_events seam_unique_events seam_duplicate_events <<< "${seam_duplicates}"
|
||||
|
||||
[[ "${seam_events}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать события DDS на стыке"
|
||||
(( seam_events > 0 )) || fail "нет событий DDS в диапазоне проверки стыка"
|
||||
(( seam_duplicate_events == 0 )) || fail "на стыке найдены дубли event_id: ${seam_duplicate_events}"
|
||||
|
||||
echo "events=${seam_events}"
|
||||
echo "unique_events=${seam_unique_events}"
|
||||
echo "duplicate_events=${seam_duplicate_events}"
|
||||
|
||||
seam_context_query="
|
||||
WITH
|
||||
toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
|
||||
t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end,
|
||||
crossing AS (
|
||||
SELECT
|
||||
e.click_id,
|
||||
min(e.event_ts) AS first_ts,
|
||||
max(e.event_ts) AS last_ts,
|
||||
groupUniqArray(coalesce(toString(e.browser_name), '__NULL__')) AS browser_names,
|
||||
groupUniqArray(coalesce(toString(e.browser_language), '__NULL__')) AS browser_languages,
|
||||
groupUniqArray(coalesce(toString(e.browser_user_agent), '__NULL__')) AS browser_user_agents,
|
||||
groupUniqArray(coalesce(toString(e.referer_url), '__NULL__')) AS referer_urls,
|
||||
groupUniqArray(coalesce(toString(e.referer_medium), '__NULL__')) AS referer_mediums,
|
||||
groupUniqArray(coalesce(toString(e.utm_medium), '__NULL__')) AS utm_mediums,
|
||||
groupUniqArray(coalesce(toString(e.utm_source), '__NULL__')) AS utm_sources,
|
||||
groupUniqArray(coalesce(toString(e.utm_content), '__NULL__')) AS utm_contents,
|
||||
groupUniqArray(coalesce(toString(e.utm_campaign), '__NULL__')) AS utm_campaigns,
|
||||
groupUniqArray(coalesce(toString(c.device_type), '__NULL__')) AS device_types,
|
||||
groupUniqArray(coalesce(toString(c.os_name), '__NULL__')) AS os_names,
|
||||
groupUniqArray(coalesce(toString(c.geo_country), '__NULL__')) AS geo_countries
|
||||
FROM dds.event AS e
|
||||
LEFT JOIN dds.click AS c ON c.click_id = e.click_id
|
||||
WHERE e.event_ts >= t_end - INTERVAL 30 MINUTE
|
||||
AND e.event_ts < t_live_end
|
||||
GROUP BY e.click_id
|
||||
HAVING first_ts < t_end AND last_ts >= t_end
|
||||
)
|
||||
SELECT
|
||||
count() AS crossing_visits,
|
||||
countIf(
|
||||
length(browser_names) = 1
|
||||
AND length(browser_languages) = 1
|
||||
AND length(browser_user_agents) = 1
|
||||
AND length(referer_urls) = 1
|
||||
AND length(referer_mediums) = 1
|
||||
AND length(utm_mediums) = 1
|
||||
AND length(utm_sources) = 1
|
||||
AND length(utm_contents) = 1
|
||||
AND length(utm_campaigns) = 1
|
||||
) AS per_event_homogeneous_visits,
|
||||
countIf(
|
||||
length(device_types) = 1
|
||||
AND length(os_names) = 1
|
||||
AND length(geo_countries) = 1
|
||||
) AS click_context_homogeneous_visits
|
||||
FROM crossing
|
||||
FORMAT TabSeparated"
|
||||
|
||||
seam_context="$(ch_query "${seam_context_query}")"
|
||||
IFS=$'\t' read -r crossing_visits per_event_homogeneous_visits click_context_homogeneous_visits <<< "${seam_context}"
|
||||
|
||||
[[ "${crossing_visits}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать переходящие визиты"
|
||||
(( crossing_visits > 0 )) || fail "нет визитов, переходящих через GEN_MODEL_T_END"
|
||||
(( per_event_homogeneous_visits == crossing_visits )) \
|
||||
|| fail "per-event фактура меняется на стыке: ${per_event_homogeneous_visits}/${crossing_visits}"
|
||||
(( click_context_homogeneous_visits == crossing_visits )) \
|
||||
|| fail "device/os/geo меняются на стыке: ${click_context_homogeneous_visits}/${crossing_visits}"
|
||||
|
||||
echo "crossing_visits=${crossing_visits}"
|
||||
echo "per_event_homogeneous_visits=${per_event_homogeneous_visits}"
|
||||
echo "click_context_homogeneous_visits=${click_context_homogeneous_visits}"
|
||||
|
||||
ods_context_query="
|
||||
WITH
|
||||
toDateTime64('${CH_MODEL_T_END}', 6) AS t_end,
|
||||
t_end + INTERVAL ${GEN_LIVE_CHECK_MINUTES} MINUTE AS t_live_end,
|
||||
crossing AS (
|
||||
SELECT
|
||||
click_id
|
||||
FROM dds.event
|
||||
WHERE event_ts >= t_end - INTERVAL 30 MINUTE
|
||||
AND event_ts < t_live_end
|
||||
GROUP BY click_id
|
||||
HAVING min(event_ts) < t_end AND max(event_ts) >= t_end
|
||||
),
|
||||
device_conflicts AS (
|
||||
SELECT
|
||||
d.click_id
|
||||
FROM ods.device_by_click AS d
|
||||
INNER JOIN crossing AS c ON c.click_id = d.click_id
|
||||
WHERE d.click_id IS NOT NULL
|
||||
GROUP BY d.click_id
|
||||
HAVING
|
||||
uniqExact(coalesce(toString(device_type), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(device_is_mobile), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(os_name), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(os), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(os_timezone), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(user_domain_id), '__NULL__')) > 1
|
||||
),
|
||||
geo_conflicts AS (
|
||||
SELECT
|
||||
g.click_id
|
||||
FROM ods.geo_by_click AS g
|
||||
INNER JOIN crossing AS c ON c.click_id = g.click_id
|
||||
WHERE g.click_id IS NOT NULL
|
||||
GROUP BY g.click_id
|
||||
HAVING
|
||||
uniqExact(coalesce(toString(geo_country), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(geo_timezone), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(geo_region_name), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(geo_latitude), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(geo_longitude), '__NULL__')) > 1
|
||||
OR uniqExact(coalesce(toString(ip_address), '__NULL__')) > 1
|
||||
)
|
||||
SELECT
|
||||
(SELECT count() FROM crossing) AS crossing_visits,
|
||||
(SELECT count() FROM device_conflicts) AS ods_device_conflicts,
|
||||
(SELECT count() FROM geo_conflicts) AS ods_geo_conflicts
|
||||
FORMAT TabSeparated"
|
||||
|
||||
ods_context="$(ch_query "${ods_context_query}")"
|
||||
IFS=$'\t' read -r ods_crossing_visits ods_device_conflicts ods_geo_conflicts <<< "${ods_context}"
|
||||
|
||||
[[ "${ods_crossing_visits}" =~ ^[0-9]+$ ]] || fail "не удалось прочитать ODS-проверку стыка"
|
||||
(( ods_crossing_visits == crossing_visits )) \
|
||||
|| fail "ODS и DDS нашли разное число переходящих визитов: ${ods_crossing_visits}/${crossing_visits}"
|
||||
(( ods_device_conflicts == 0 )) || fail "ODS device конфликтует на стыке: ${ods_device_conflicts}"
|
||||
(( ods_geo_conflicts == 0 )) || fail "ODS geo конфликтует на стыке: ${ods_geo_conflicts}"
|
||||
|
||||
echo "ods_device_conflicts=${ods_device_conflicts}"
|
||||
echo "ods_geo_conflicts=${ods_geo_conflicts}"
|
||||
else
|
||||
echo ""
|
||||
echo "Проверка стыка backfill/live пропущена: CHECK_LIVE_SEAM=${CHECK_LIVE_SEAM}, live_rows=${live_rows:-0}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Проверка ClickHouse: основные DM-витрины не пустые ==="
|
||||
|
||||
|
||||
Reference in New Issue
Block a user