Оптимизация рекламы в Яндекс.Директ только на целевые лиды без CRM: Пишем свой коннектор на Python и GAS

Оптимизация рекламы в Яндекс.Директ только на целевые лиды без CRM: Пишем свой коннектор на Python и GAS

Всех приветствую! С апреля я плотно работаю над большим проектом по автоматизации рекламы (о нем я писал тут). За это время я реализовал множество функций, которые ежедневно экономят мне часы на оптимизации кампаний.

Сейчас основная версия уже разработана, я работаю над облегченной версией с урезанным функционалом, с которой я планирую поделиться со всеми и выкатить подробный гайд по внедрению. Хочу успеть до нового года, чтобы порадовать и сделать небольшой подарок вам.

Ну а сегодня я хочу поделиться интересным методом работы с офлайн-конверсиями. Это особенно актуально для тех проектов, где нет сложной сквозной аналитики или CRM-системы.

Кроме того, этот метод помогает бороться за качество трафика. При работе со стратегией "Оплата за конверсии" алгоритмы часто приводят дешевые, но "мусорные" заявки. Мой подход позволяет платить только за тех, кто реально прошел квалификацию.

Прежде чем я покажу, как собрать такую систему, давайте разберемся, как она работает. Допустим, на вашем сайте установлен коллтрекинг и форма заявки. Эти две цели вы используете для оптимизации кампаний и опираетесь на них в отчетах.

Суть метода заключается в том, чтобы создать в Метрике дополнительную JavaScript-цель. Выполнение этой цели мы будем передавать вручную и только после того, как менеджер квалифицировал лид и пометил его как целевой.

Чтобы реализовать эту идею нам понадобится:

  • JS-цель в Яндекс.Метрике.

  • API-токен Яндекс.Метрики (для доступа к отчетам).

  • Google Таблица для учета данных.

  • Доступы к Google API (файл json с ключами сервисного аккаунта).

  • Проект на Python (наш основной коннектор).

  • Скрипт на GAS (Google Apps Script) для внутренней автоматизации.

Создаем цель в Яндекс. Метрике

Для начала создаем цель в Яндекс. Метрике со следующими параметрами:

Оптимизация рекламы в Яндекс.Директ только на целевые лиды без CRM: Пишем свой коннектор на Python и GAS
Тип: JavaScript-событие. Идентификатор цели: lead_approved ❗️ Важно: Убедитесь, что тип совпадения выбран "Совпадает".

Получение API-токена для Яндекс.Метрики

Процесс регистрации приложения и получения токена я подробно разбирал в этой статье. Чтобы не дублировать информацию, рекомендую ознакомиться с инструкцией по ссылке.

Здесь отмечу лишь важный нюанс: при настройке доступов убедитесь, что у вашего приложения проставлены следующие разрешения (права):

  • Создание счётчиков, изменение параметров своих и доверенных счётчиков.
  • Загрузка параметров пользователей.
  • Загрузка офлайн данных.
  • Получение статистики, чтение параметров своих и доверенных счётчиков.

Возможно, некоторые права из этого списка покажутся избыточными для конкретной задачи передачи конверсий. Однако я использую единое приложение для всего стека автоматизации, поэтому рекомендую отметить их все сразу. Это застрахует вас от ошибок доступа («Access denied») при расширении функционала в будущем.

Cоздание Google таблицы

После того, как вы создали свое приложение, выдали доступы и получили токен теперь необходимо создать таблицу:

  1. Назовите ее, например, "Учет конверсий".
  2. Скопируйте название таблицы (оно понадобится для скрипта).
  3. Оставьте ее пока пустой, скрипт сам создаст нужные листы и заголовки.

Получаем доступ к Google API

Чтобы Python мог писать данные в вашу таблицу, нужно создать "Сервисный аккаунт". Это самый сложный этап для новичков, поэтому идите строго по пунктам.

  1. Зайдите в Google Cloud Console.
  2. Создайте новый проект (New Project), назовите его, например, MetrikaConnector.
  3. Включение API: В меню слева выберите APIs & Services -> Library.В поиске введите Google Sheets API, нажмите на него и нажмите Enable. Вернитесь в Library, найдите Google Drive API и тоже нажмите Enable.
  4. Создание доступов: Перейдите в APIs & Services -> Credentials. Нажмите + CREATE CREDENTIALS -> Service Account.Придумайте имя (например, Api_metrika), нажмите Create and Continue, затем Done.
  5. Получение ключа: В списке Service Accounts нажмите на только что созданный аккаунт (email адрес). Перейдите на вкладку Keys. Нажмите Add Key -> Create new key. Выберите тип JSON и нажмите Create. Файл автоматически скачается на компьютер. Переименуйте его в google_credentials.json.
  6. ВАЖНО: Доступ к таблице: Откройте скачанный JSON-файл в блокноте. Найдите строчку "client_email": "...". Скопируйте этот email (он длинный и странный).
  7. Откройте вашу созданную Google Таблицу. Нажмите кнопку "Настройки доступа" и вставьте скопированный email и дайте права Редактор (Editor).

Создаем основной проект на Python

Теперь, когда подготовительный этап завершен, можно приступать к сборке Python-проекта и первому тесту.

  • Создайте новую папку в любом удобном месте (назовите её, например, lead_approved).
  • Первым делом переместите в неё файл google_credentials.json, который вы скачали из Google Cloud Console.

Если вы ранее не использовали Python, вам необходимо его скачать:

  1. Перейдите на официальный сайт python.org/downloads.

  2. Нажмите большую желтую кнопку Download Python (сайт сам определит вашу операционную систему).

  3. Запустите скачанный установочный файл.

  4. 🛑 ВНИМАНИЕ (Для Windows): В окне установки обязательно поставьте галочку напротив пункта «Add Python to PATH» (или «Add Python to environment variables»). Если этого не сделать, командная строка не поймет команду python, и скрипт не запустится.

  5. Нажмите Install Now и дождитесь окончания.

  1. Откройте командную строку (Терминал):Windows (Самый простой способ): Зайдите в папку с вашим проектом (где лежат файлы), кликните левой кнопкой мыши в адресную строку проводника (там, где путь к папке), напишите cmd и нажмите Enter. Откроется черное окно сразу в нужной папке. macOS: Откройте Terminal, напишите cd (с пробелом на конце) и перетащите вашу папку из Finder прямо в окно терминала. Путь подставится сам. Нажмите Enter.
  2. Создаем виртуальное окружение: В командной строке введите: python -m venv venv (Если вы на macOS / Linux и команда не сработала, попробуйте python3 -m venv venv). После этого в вашей папке появится новая папка venv.
  3. Активируем окружение: Теперь нужно "войти" в это пространство. Команды отличаются для разных систем: Windows:venv\Scripts\activate macOS / Linux: source venv/bin/activate. Если всё прошло успешно, в начале строки в терминале появится надпись в скобках: (venv).
  4. Установка библиотек:Теперь, находясь внутри изолированного окружения, установим необходимые библиотеки (requests для запросов к API иgspread для таблиц). Остальные модули (json,datetime) уже встроены в Python. Введите команду:
pip install requests gspread

Вы увидите процесс загрузки. Когда он закончится, ваша среда полностью готова к запуску скрипта.

Для написания кода нам понадобится удобный текстовый редактор. Можно использовать и стандартный "Блокнот", но есть риск ошибиться с кодировкой или расширением файла (например, случайно сохранить как .py.txt).

Лично я использую Sublime Text — он легкий, быстрый и отлично подходит для таких задач. Если у вас еще нет любимого редактора кода, рекомендую скачать и установить именно его.

Откройте Sublime Text (или ваш редактор), нажмите File -> New File и создайте внутри папки вашего проекта два файла:

Файл настроек: client_goal.json

Создайте новый файл, вставьте в него код ниже и сохраните под именем client_goal.json. Здесь мы будем хранить список проектов, ID счетчиков и цели, которые считаем "квалификационными" (например, отправка формы или звонок).

[ { "client_name": "Proekt_A", "counter_id": "ВАШ_ID_СЧЕТЧИКА", "goal_ids": [ "ID_ЦЕЛИ_1", "ID_ЦЕЛИ_2" ] } ]
  • client_name: Название листа, который создастся в Гугл Таблице.
  • counter_id: Номер счетчика Яндекс.Метрики.
  • goal_ids: ID целей из Метрики (через запятую, если их несколько), при срабатывании которых мы заносим лид в таблицу на проверку.

Основной скрипт: main.py

Создайте второй файл, вставьте в него весь код скрипта (приведен ниже) и сохраните как main.py.

Важно: Не забудьте вставить в начале файла (в блоке настроек) ваш OAuth-токен от Метрики и точное название вашей Google таблицы.

import json import requests import gspread from datetime import date, timedelta, datetime # --- 1. ГЛОБАЛЬНЫЕ НАСТРОЙКИ --- METRIKA_TOKEN = "ВСТАВЬТЕ_СЮДА_ВАШ_ТОКЕН" CONFIG_FILE = 'client_goal.json' GOOGLE_CREDENTIALS_FILE = 'google_credentials.json' SPREADSHEET_NAME = "ВСТАВЬТЕ_СЮДА_НАЗВАНИЕ_ВАШЕЙ_ТАБЛИЦЫ" # ------------------------------------ # --- НОВОЕ: Заголовки для новой таблицы --- SHEET_HEADERS = [ 'timestamp', 'client_name', 'counter_id', 'clientID', 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'status', 'upload_id' ] # ------------------------------------------- def get_metrika_reports(counter_id, goal_ids, date1, date2, client_name): # <-- Добавлен client_name для логгирования """ Запрашивает у Reports API Метрики визиты с нужными целями за указанный период. """ # Теперь client_name передается в функцию напрямую print(f"- Обрабатываю '{client_name}' (счетчик {counter_id})...") api_url = "https://api-metrika.yandex.net/stat/v1/data" headers = {'Authorization': f'OAuth {METRIKA_TOKEN}'} goal_filters = [f"ym:s:goal{goal_id}IsReached=='Yes'" for goal_id in goal_ids] filters_string = " OR ".join(goal_filters) params = { "ids": counter_id, "metrics": "ym:s:visits", "dimensions": "ym:s:dateTime,ym:s:clientID,ym:s:UTMSource,ym:s:UTMMedium,ym:s:UTMCampaign,ym:s:UTMTerm", "date1": date1, "date2": date2, "filters": filters_string, "accuracy": "full", "limit": 100000 } try: response = requests.get(api_url, headers=headers, params=params) response.raise_for_status() response_json = response.json() data = response_json.get('data', []) if not data: print(" Лиды за этот период не найдены.") return [] visits = [] for item in data: dims = item['dimensions'] visits.append({ 'date': dims[0]['name'], 'clientID': dims[1]['name'], 'utm_source': dims[2]['name'], 'utm_medium': dims[3]['name'], 'utm_campaign': dims[4]['name'], 'utm_term': dims[5]['name'], }) print(f" Найдено лидов: {len(visits)}") return visits except requests.exceptions.RequestException as e: print(f" ! Ошибка при запросе к Reports API: {e}") try: print(" Ответ сервера:", e.response.json()) except: pass return [] # --- ИЗМЕНЕНА ФУНКЦИЯ --- def update_google_sheet(leads, client_name): """ Обновляет Google Таблицу. Находит лист по client_name или создает новый, если лист не найден, и добавляет в него данные. """ if not leads: print("\nНет новых лидов для добавления в таблицу.") return print(f"\nПодключаюсь к Google Таблице '{SPREADSHEET_NAME}'...") try: gc = gspread.service_account(filename=GOOGLE_CREDENTIALS_FILE) spreadsheet = gc.open(SPREADSHEET_NAME) # --- НОВАЯ ЛОГИКА: Поиск или создание вкладки (листа) --- try: worksheet = spreadsheet.worksheet(client_name) print(f"Найден лист (вкладка) с именем '{client_name}'. Добавляю данные...") except gspread.exceptions.WorksheetNotFound: print(f"Лист с именем '{client_name}' не найден. Создаю новый...") # Создаем новый лист и добавляем в него заголовки worksheet = spreadsheet.add_worksheet(title=client_name, rows=1, cols=len(SHEET_HEADERS)) worksheet.append_row(SHEET_HEADERS) print(f"Лист '{client_name}' успешно создан.") # -------------------------------------------------------- rows_to_add = [] for lead in leads: # Порядок данных должен строго соответствовать заголовкам SHEET_HEADERS rows_to_add.append([ lead['date'], lead['client_name'], lead['counter_id'], lead['clientID'], lead['utm_source'], lead['utm_medium'], lead['utm_campaign'], lead['utm_term'], 'new' # status ]) if rows_to_add: worksheet.append_rows(rows_to_add, value_input_option='USER_ENTERED') print(f"Успешно добавлено {len(rows_to_add)} строк в лист '{client_name}'.") else: print("Нет строк для добавления.") except Exception as e: print(f" ! Ошибка при работе с Google Таблицей: {e}") def get_date_from_user(prompt): """Запрашивает у пользователя дату и проверяет ее корректность.""" while True: date_str = input(prompt) try: datetime.strptime(date_str, '%Y-%m-%d') return date_str except ValueError: print("Неверный формат. Пожалуйста, введите дату в формате ГГГГ-ММ-ДД (например, 2025-11-20)") # --- ИЗМЕНЕН ГЛАВНЫЙ БЛОК --- if __name__ == "__main__": try: with open(CONFIG_FILE, 'r', encoding='utf-8') as f: client_configs = json.load(f) except FileNotFoundError: print(f"Ошибка: Файл конфигурации '{CONFIG_FILE}' не найден.") exit() # --- НОВАЯ ЛОГИКА: Выбор клиента --- client_names = [config.get("client_name") for config in client_configs if config.get("client_name")] if not client_names: print("В файле конфигурации не найдено ни одного клиента.") exit() print("--- Выбор клиента для выгрузки ---") print("Доступные клиенты:", ", ".join(client_names)) selected_client_name = None selected_config = None while not selected_config: user_input = input("Введите имя клиента, для которого хотите сделать выгрузку: ") # Ищем конфиг для введенного клиента for config in client_configs: if config.get("client_name", "").lower() == user_input.lower(): selected_config = config selected_client_name = config.get("client_name") break if not selected_config: print(f"Клиент с именем '{user_input}' не найден. Попробуйте еще раз.") print(f"\nВыбран клиент: '{selected_client_name}'.") # --------------------------------------------- # --- БЛОК ЗАПРОСА ДАТ: остался без изменений --- print("\n--- Настройка периода выгрузки ---") date_start = get_date_from_user("Введите дату начала (ГГГГ-ММ-ДД): ") date_end = get_date_from_user("Введите дату конца (ГГГГ-ММ-ДД): ") print(f"Выбран период с {date_start} по {date_end}.\n") # ---------------------------------------------------- all_leads_to_upload = [] print("--- Начинаю сбор лидов ---") # Теперь работаем только с выбранной конфигурацией, а не в цикле counter_id = selected_config.get("counter_id") goal_ids = selected_config.get("goal_ids") if counter_id and goal_ids: # Передаем даты и имя клиента в функцию сбора данных leads_from_metrika = get_metrika_reports(counter_id, goal_ids, date_start, date_end, selected_client_name) for lead in leads_from_metrika: lead['client_name'] = selected_client_name lead['counter_id'] = counter_id all_leads_to_upload.append(lead) # Передаем лиды и имя клиента в функцию обновления таблицы update_google_sheet(all_leads_to_upload, selected_client_name) else: print(f"В конфигурации для '{selected_client_name}' отсутствуют counter_id или goal_ids.") print("\nРабота скрипта завершена.")
  • METRIKA_TOKEN: Вставьте сюда ваш токен
  • SPREADSHEET_NAME: Вставьте сюда название вашей Google таблицы

Начинаем тестировать

1. Открываем терминал и переходим в папку

  • Для Windows: Нажмите Win + R, введите cmd и нажмите Enter.
  • Для macOS: Нажмите Cmd + Space, введите Terminal и откройте его.

Теперь нужно «зайти» в папку проекта. Введите команду cd, поставьте пробел и просто перетащите папку проекта прямо в черное окно терминала — путь подставится сам.

Нажмите Enter.

2. Активируем виртуальное окружение

Если этот шаг пропустить, скрипт не найдет установленные библиотеки.

  • Windows:
venv\Scripts\activate
  • macOS / Linux:
source venv/bin/activate

3. Запускаем скрипт

python main.py

Скрипт запросит имя клиента (как в файле настроек) и даты. После ввода данных скрипт создаст новый лист в вашей Google Таблице и заполнит его конверсиями.

Давайте внимательней посмотрим на выгрузку:

Оптимизация рекламы в Яндекс.Директ только на целевые лиды без CRM: Пишем свой коннектор на Python и GAS

Открыв таблицу, помимо привычных UTM-меток и ClientID, вы заметите два служебных столбца, которые скрипт создал автоматически. В них кроется вся суть метода.

  1. status (Статус обработки)По умолчанию скрипт ставит всем новым лидам статус new. Задача менеджера отдела продаж: Проверить эти лиды (прозвонить/сравнить с CRM) и изменить статус качественных заявок на "da" (или любой другой, который мы зададим в коде).Именно изменение статуса станет "спусковым крючком" для отправки конверсии в Яндекс.
  2. upload_id (ID выгрузки) Сейчас этот столбец пуст. Он нужен для защиты от дублей. Когда мы настроим финальную автоматизацию, скрипт будет записывать сюда уникальный идентификатор после успешной отправки данных в Метрику. Если в ячейке есть запись — система поймет, что этот лид уже учтен.

Сама по себе таблица — это просто хранилище данных. Чтобы все сработало и Яндекс узнал о целевых лидах, нам нужно связать Google Таблицу с API Метрики. Делать это мы будем прямо внутри таблицы с помощью Google Apps Script (GAS).

Скрипт для GAS

1. Открываем редактор скриптов. Находясь в вашей Google Таблице, в верхнем меню нажмите: Расширения (Extensions) -> Apps Script.

Откроется новая вкладка с редактором кода. Удалите всё, что там есть (обычно это пустая функция myFunction), и вставьте код ниже целиком.

// --- НАСТРОЙКИ --- const AUTH_TOKEN = "ВАШ_AOUTH_TOKEN"; // <-- ВАШ ПОЛНЫЙ OAuth ТОКЕН const TRIGGER_STATUS = "da"; // <-- СИМВОЛ-ТРИГГЕР // --------------------------------------------------- // --- НОМЕРА СТОЛБЦОВ --- // A=1, B=2, C=3 (counter_id), D=4, E=5, F=6 (clientID), ... K=11 (status), L=12 (upload_id) const COUNTER_ID_COLUMN = 3; // Столбец C (counter_id) const CLIENT_ID_COLUMN = 4; // Столбец F (clientID) const STATUS_COLUMN = 9; // Столбец K (status) const UPLOAD_ID_COLUMN = 10; // Столбец L (upload_id) const GOAL_ID = "lead_approved"; // --- НАСТРОЙКИ ДЛЯ ПРОВЕРКИ СТАТУСА --- const STATUS_SENT = "Отправлено"; const STATUS_SUCCESS = "Успешно"; const STATUS_ERROR = "Ошибка"; const COLOR_SUCCESS = "#d9ead3"; // Светло-зеленый const COLOR_ERROR = "#f4cccc"; // Светло-красный const COLOR_NEUTRAL = "#ffffff"; // Белый (для сброса) // ---------------------------------------------------- /** * ======================================================================================= * === БЛОК 1: ФУНКЦИИ ОТПРАВКИ КОНВЕРСИЙ (СРАБАТЫВАЮТ ПРИ РЕДАКТИРОВАНИИ) === * ======================================================================================= */ function processEdit(e) { Logger.log("--- Запуск скрипта отправки конверсии ---"); // --- ИСПРАВЛЕНИЕ: Возвращаем getActiveRange(), чтобы работало и при ручном запуске --- const range = SpreadsheetApp.getActiveRange(); const sheet = range.getSheet(); const newValue = range.getValue().toString() || ""; // Условие срабатывания остается прежним: редактирование нужного столбца if (range.getColumn() === STATUS_COLUMN) { if (newValue.trim().toLowerCase() === TRIGGER_STATUS) { const row = range.getRow(); // Пропускаем заголовок if (row === 1) return; const counterID = sheet.getRange(row, COUNTER_ID_COLUMN).getValue(); const clientID = sheet.getRange(row, CLIENT_ID_COLUMN).getValue(); if (clientID && counterID) { Logger.log(`Лист: "${sheet.getName()}", Строка: ${row}. Статус изменен на '${TRIGGER_STATUS}'. ClientID: ${clientID}, CounterID: ${counterID}. Запускаю отправку.`); sendConversionToMetrika(clientID, counterID, sheet, row); } else { Logger.log(`Лист: "${sheet.getName()}", Строка: ${row}. Статус изменен, но ClientID или CounterID пустой. Отправка отменена.`); } } } } /** * Отправляет данные о конверсии в API Яндекс.Метрики в формате CSV. * Эта функция остается БЕЗ ИЗМЕНЕНИЙ, она универсальна. * @param {string} clientID Идентификатор клиента для отправки. * @param {string} counterID Идентификатор счетчика, куда отправлять конверсию. * @param {GoogleAppsScript.Spreadsheet.Sheet} [sheet] - Активный лист * @param {number} [row] - Номер текущей строки */ function sendConversionToMetrika(clientID, counterID, sheet, row) { const url = `https://api-metrika.yandex.ru/management/v1/counter/${counterID}/offline_conversions/upload?client_id_type=CLIENT_ID`; const cleanedClientID = clientID.toString().trim(); const boundary = "------------------------" + Math.random().toString(36).substring(2); const timestamp = Math.floor(new Date().getTime() / 1000); const fileContent = `ClientId,Target,DateTime\n${cleanedClientID},${GOAL_ID},${timestamp}`; const payload = "--" + boundary + "\r\n" + "Content-Disposition: form-data; name=\"file\"; filename=\"data.csv\"\r\n" + "Content-Type: text/csv\r\n\r\n" + fileContent + "\r\n" + "--" + boundary + "--"; const options = { 'method': 'POST', 'contentType': 'multipart/form-data; boundary=' + boundary, 'headers': { 'Authorization': `OAuth ${AUTH_TOKEN}` }, 'payload': Utilities.newBlob(payload).getBytes(), 'muteHttpExceptions': true }; try { const response = UrlFetchApp.fetch(url, options); const responseCode = response.getResponseCode(); const responseBody = response.getContentText(); if (responseCode === 200) { const jsonResponse = JSON.parse(responseBody); const uploadId = jsonResponse.uploading.id; Logger.log(`✅ УСПЕХ! Данные для ClientID ${cleanedClientID} отправлены. Получен Upload ID: ${uploadId}`); if (sheet && row) { sheet.getRange(row, STATUS_COLUMN).setValue(STATUS_SENT); sheet.getRange(row, UPLOAD_ID_COLUMN).setValue(uploadId); sheet.getRange(row, STATUS_COLUMN).setBackground(COLOR_NEUTRAL); } } else { Logger.log(`Ошибка отправки в счетчик ${counterID}. Код: ${responseCode}. Ответ: ${responseBody}`); if (sheet && row) { sheet.getRange(row, STATUS_COLUMN).setValue(STATUS_ERROR).setBackground(COLOR_ERROR); } } } catch (error) { Logger.log('Критическая ошибка: ' + error.toString()); if (sheet && row) { sheet.getRange(row, STATUS_COLUMN).setValue(STATUS_ERROR).setBackground(COLOR_ERROR); } } } /** * =============================================================================== * === БЛОК 2: ФУНКЦИИ ДЛЯ ПРОВЕРКИ СТАТУСА (ЗАПУСКАЮТСЯ ПО ТРИГГЕРУ) === * =============================================================================== */ /** * Главная функция, которую нужно будет запускать по триггеру (например, раз в час). * --- ИЗМЕНЕНИЕ: Теперь она проходит по всем листам в таблице. --- */ function checkConversionStatuses() { Logger.log("--- Запуск проверки статусов офлайн-конверсий по всем листам ---"); // Получаем все листы (вкладки) в текущей таблице const allSheets = SpreadsheetApp.getActiveSpreadsheet().getSheets(); // Проходим циклом по каждому листу for (const sheet of allSheets) { Logger.log(`\n>>> Обрабатываю лист: "${sheet.getName()}"`); // Пропускаем пустые листы или листы только с заголовком if (sheet.getLastRow() < 2) { Logger.log("Лист пустой, пропускаю."); continue; } const dataRange = sheet.getRange(2, 1, sheet.getLastRow() - 1, sheet.getLastColumn()); const values = dataRange.getValues(); let foundToCheck = 0; // Счетчик для логов for (let i = 0; i < values.length; i++) { const row = i + 2; const status = values[i][STATUS_COLUMN - 1]; const uploadId = values[i][UPLOAD_ID_COLUMN - 1]; const counterId = values[i][COUNTER_ID_COLUMN - 1]; if (status === STATUS_SENT && uploadId && counterId) { foundToCheck++; Logger.log(` Проверяю строку ${row}: CounterID=${counterId}, UploadID=${uploadId}`); const statusResponse = getMetrikaUploadStatus(counterId, uploadId); if (statusResponse) { const metrikaStatus = statusResponse.uploading.status; const statusCell = sheet.getRange(row, STATUS_COLUMN); Logger.log(` Статус от Метрики: ${metrikaStatus}`); if (metrikaStatus === "PROCESSED") { statusCell.setValue(STATUS_SUCCESS).setBackground(COLOR_SUCCESS); Logger.log(` ✅ Строка ${row} успешно обработана.`); } else if (metrikaStatus === "FAILED") { const errorMessage = statusResponse.uploading.comment || "Нет деталей"; statusCell.setValue(STATUS_ERROR).setBackground(COLOR_ERROR); statusCell.setComment(`Ошибка от API: ${errorMessage}`); Logger.log(` ❌ Ошибка в строке ${row}. Комментарий: ${errorMessage}`); } } } } if (foundToCheck === 0) { Logger.log("На этом листе не найдено строк со статусом 'Отправлено'."); } } Logger.log("\n--- Проверка статусов завершена ---"); } /** * Запрашивает у API Метрики статус загрузки по ее ID. * Эта функция остается БЕЗ ИЗМЕНЕНИЙ, она универсальна. * @param {string} counterId Идентификатор счетчика. * @param {string} uploadId Идентификатор загрузки. * @returns {object|null} Объект ответа API или null в случае ошибки. */ function getMetrikaUploadStatus(counterId, uploadId) { const url = `https://api-metrika.yandex.ru/management/v1/counter/${counterId}/offline_conversions/uploading/${uploadId}`; const options = { 'method': 'GET', 'headers': { 'Authorization': `OAuth ${AUTH_TOKEN}` }, 'muteHttpExceptions': true }; try { const response = UrlFetchApp.fetch(url, options); if (response.getResponseCode() === 200) { return JSON.parse(response.getContentText()); } else { Logger.log(` ! Ошибка при запросе статуса для UploadID ${uploadId}. Код: ${response.getResponseCode()}, Ответ: ${response.getContentText()}`); return null; } } catch (error) { Logger.log(` ! Критическая ошибка при запросе статуса: ${error.toString()}`); return null; } }

В самой первой строке кода const AUTH_TOKEN = "..." обязательно замените текст в кавычках на ваш реальный OAuth-токен. Без этого скрипт не сработает.

Нажмите на иконку дискеты, чтобы сохранить проект. Назовите его, например, "Коннектор Метрики".

Настройка триггеров

Код написан, но он пока не знает, когда запускаться. Нам нужно добавить два "триггера" — события, которые будут будить наш скрипт.

В левом меню редактора нажмите на иконку будильника (Триггеры / Triggers), затем синюю кнопку «Добавление триггера» справа внизу.

Триггер №1: Мгновенная отправка

Этот триггер будет следить за вашими изменениями в таблице. Как только вы напишете da в статусе, он отправит данные.

  • Выберите функцию: processEdit
  • Выберите источник мероприятия: Из таблицы (From spreadsheet)
  • Выберите тип события: При редактировании (On edit)
  • Нажмите Сохранить.(Google попросит предоставить разрешения. Выберите свой аккаунт -> Advanced (Дополнительно) -> Go to ... (unsafe) -> Allow (Разрешить). Это стандартная процедура для своих скриптов).

Триггер №2: Проверка статуса обработки

Метрика обрабатывает загруженные файлы не сразу (иногда от 10 минут до 2 часов). Чтобы не проверять это вручную, настроим таймер.

Добавьте еще один триггер:

  • Выберите функцию: checkConversionStatuses
  • Выберите источник мероприятия: Триггер по времени (Time-driven)
  • Выберите тип триггера: Часовой таймер (Hour timer)
  • Выберите интервал: Раз в час (Every hour)
  • Нажмите Сохранить.

Давайте протестируем

Тестируем триггер функции processEdit:

Как видите, данные передались в метрику, осталось только подождать и увидеть финальный статус. Это займет какое-то время.

Оптимизация рекламы в Яндекс.Директ только на целевые лиды без CRM: Пишем свой коннектор на Python и GAS

Чтобы убедиться окончательно, переходим в Яндекс.Метрику -> Отчеты -> Конверсии. Найдите вашу цель lead_approved. Если вы видите там что появились данные — значит все заработало.

Оптимизация рекламы в Яндекс.Директ только на целевые лиды без CRM: Пишем свой коннектор на Python и GAS

Готово! Теперь у вас есть полностью автономная система передачи офлайн-конверсий, которая не требует ежемесячной оплаты за дорогие коннекторы.

Начать дискуссию